From 1fedb47df3d5e2380e7d9a6bf4ce31d6a0255095 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Sat, 17 Aug 2019 03:32:11 +0200 Subject: [PATCH] port AWS providers to sdk v3 --- lib/dpl/providers/codedeploy.rb | 3 +- lib/dpl/providers/elasticbeanstalk.rb | 7 +- lib/dpl/providers/lambda.rb | 2 +- lib/dpl/providers/opsworks.rb | 2 +- lib/dpl/providers/s3.rb | 4 +- spec/dpl/providers/codedeploy_spec.rb | 48 ++--- spec/dpl/providers/elasticbeanstalk_spec.rb | 90 ++++----- spec/dpl/providers/lambda_spec.rb | 147 +++++++-------- spec/dpl/providers/opsworks_spec.rb | 142 ++++++--------- spec/dpl/providers/s3_spec.rb | 76 +++----- spec/spec_helper.rb | 1 + spec/support.rb | 17 +- spec/support/helpers.rb | 31 ++++ spec/support/matchers.rb | 1 + spec/support/matchers/aws.rb | 192 ++++++++++++++++++++ 15 files changed, 436 insertions(+), 327 deletions(-) create mode 100644 spec/support/helpers.rb create mode 100644 spec/support/matchers/aws.rb diff --git a/lib/dpl/providers/codedeploy.rb b/lib/dpl/providers/codedeploy.rb index a31955533..65b04c8d7 100644 --- a/lib/dpl/providers/codedeploy.rb +++ b/lib/dpl/providers/codedeploy.rb @@ -11,7 +11,8 @@ class Codedeploy < Provider tbd str - gem 'aws-sdk', '~> 2.0' + gem 'aws-sdk-codedeploy', '~> 1.0' + gem 'aws-sdk-s3', '~> 1.0' env :aws config '~/.aws/credentials', '~/.aws/config', prefix: 'aws' diff --git a/lib/dpl/providers/elasticbeanstalk.rb b/lib/dpl/providers/elasticbeanstalk.rb index 90e20e036..36b77fd34 100644 --- a/lib/dpl/providers/elasticbeanstalk.rb +++ b/lib/dpl/providers/elasticbeanstalk.rb @@ -9,7 +9,8 @@ class Elasticbeanstalk < Provider tbd str - gem 'aws-sdk', '~> 2.0' + gem 'aws-sdk-elasticbeanstalk', '~> 1.0' + gem 'aws-sdk-s3', '~> 1.0' gem 'rubyzip', '~> 1.2.2', require: 'zip' gem 'pathspec', '~> 0.2.1', require: 'pathspec' @@ -126,8 +127,8 @@ def wait_until_deployed def check_deployment(msgs) sleep 5 events.each do |event| - msg = "#{event[:event_date]} [#{event[:severity]}] #{event[:message]}" - error "Deployment failed: #{msg}" if event[:severity] == 'ERROR' + msg = "#{event.event_date} [#{event.severity}] #{event.message}" + error "Deployment failed: #{msg}" if event.severity == 'ERROR' info msg unless msgs.include?(msg) msgs << msg end diff --git a/lib/dpl/providers/lambda.rb b/lib/dpl/providers/lambda.rb index fa0f248e8..fc8a63e9e 100644 --- a/lib/dpl/providers/lambda.rb +++ b/lib/dpl/providers/lambda.rb @@ -11,7 +11,7 @@ class Lambda < Provider tbd str - gem 'aws-sdk', '~> 2.0' + gem 'aws-sdk-lambda', '~> 1.0' gem 'rubyzip', '~> 1.2.2', require: 'zip' env :aws diff --git a/lib/dpl/providers/opsworks.rb b/lib/dpl/providers/opsworks.rb index 8e2f30655..a3d9ec75b 100644 --- a/lib/dpl/providers/opsworks.rb +++ b/lib/dpl/providers/opsworks.rb @@ -9,7 +9,7 @@ class Opsworks < Provider tbd str - gem 'aws-sdk', '~> 2.0' + gem 'aws-sdk-opsworks', '~> 1.0' env :aws config '~/.aws/credentials', '~/.aws/config', prefix: 'aws' diff --git a/lib/dpl/providers/s3.rb b/lib/dpl/providers/s3.rb index 2f90e07f9..695519458 100644 --- a/lib/dpl/providers/s3.rb +++ b/lib/dpl/providers/s3.rb @@ -14,7 +14,7 @@ class S3 < Provider tbd str - gem 'aws-sdk', '~> 2.0', require: ['aws-sdk', 'dpl/support/aws_sdk_patch'] + gem 'aws-sdk-s3', '~> 1.0' gem 'mime-types', '~> 3.2.2' env :aws @@ -57,7 +57,7 @@ class S3 < Provider def setup @cwd = Dir.pwd Dir.chdir(local_dir) - Aws.eager_autoload!(services: ['S3']) + # Aws.eager_autoload!(services: ['S3']) end def login diff --git a/spec/dpl/providers/codedeploy_spec.rb b/spec/dpl/providers/codedeploy_spec.rb index 7404fdac8..bef66f36d 100644 --- a/spec/dpl/providers/codedeploy_spec.rb +++ b/spec/dpl/providers/codedeploy_spec.rb @@ -1,43 +1,31 @@ describe Dpl::Providers::Codedeploy do - let(:args) { |e| %w(--access_key_id access_key_id --secret_access_key secret_access_key --application app) + args_from_description(e) } - let(:requests) { Hash.new { |hash, key| hash[key] = [] } } + include Support::Matchers::Aws - env TRAVIS_BUILD_NUMBER: 1 + let(:args) { |e| %w(--access_key_id access_key_id --secret_access_key secret_access_key --application app) + args_from_description(e) } + let(:client) { Aws::CodeDeploy::Client.new(stub_responses: responses[:eb]) } + let(:s3) { Aws::S3::Client.new(stub_responses: true) } - before do - Aws.config[:s3] = { - stub_responses: { - get_object: ->(ctx) { - requests[:buckets] << ctx.http_request - { deployment_id: 'deployment_id' } - } - } - } - Aws.config[:codedeploy] = { - stub_responses: { - create_deployment: ->(ctx) { - requests[:create_deployment] << ctx.http_request - { deployment_id: 'deployment_id' } + let(:responses) do + { + eb: { + create_deployment: { + deployment_id: 'deployment_id' }, - get_deployment: ->(ctx) { - requests[:get_deployment] << ctx.http_request - { deployment_info: { status: 'Succeeded' } } + get_deployment: { + deployment_info: { status: 'Succeeded' } } } } end - matcher :create_deployment do |params = {}| - match do |*| - next false unless request = requests[:create_deployment][0] - body = symbolize(JSON.parse(request.body.read)) - params.all? { |key, value| body[key] == value } - end - end + let(:github_revision) { { revisionType: 'GitHub', gitHubLocation: { repository: 'dpl', commitId: 'sha' } } } + let(:s3_revision) { { revisionType: 'S3', s3Location: { bucket: 'bucket', bundleType: 'zip', version: 'ObjectVersionId', eTag: 'ETag' } } } - matcher :get_deployment do |*| - match { |*| requests[:get_deployment].any? } - end + env TRAVIS_BUILD_NUMBER: 1 + + before { allow(Aws::CodeDeploy::Client).to receive(:new).and_return(client) } + before { allow(Aws::S3::Client).to receive(:new).and_return(s3) } + before { |c| subject.run unless c.example_group.metadata[:run].is_a?(FalseClass) } let(:github_revision) { { revisionType: 'GitHub', gitHubLocation: { repository: 'dpl', commitId: 'sha' } } } let(:s3_revision) { { revisionType: 'S3', s3Location: { bucket: 'bucket', bundleType: 'zip', version: 'ObjectVersionId', eTag: 'ETag' } } } diff --git a/spec/dpl/providers/elasticbeanstalk_spec.rb b/spec/dpl/providers/elasticbeanstalk_spec.rb index 0d6b292ee..458bbebd0 100644 --- a/spec/dpl/providers/elasticbeanstalk_spec.rb +++ b/spec/dpl/providers/elasticbeanstalk_spec.rb @@ -1,74 +1,56 @@ describe Dpl::Providers::Elasticbeanstalk do + include Support::Matchers::Aws + let(:args) { |e| required + args_from_description(e) } let(:required) { %w(--access_key_id id --secret_access_key key --env env --bucket bucket) } - let(:requests) { Hash.new { |hash, key| hash[key] = [] } } let(:events) { [] } - matcher :create_app_version do |opts = {}| - match do |*| - next false unless requests[:create_application_version].any? - next true unless opts[:with] - body = requests[:create_application_version][0].body.read - opts[:with].is_a?(Regexp) ? body =~ opts[:with] : body.include?(opts[:with]) - end - end - - matcher :update_environment do - match do - requests[:update_environment].any? - end - end + let(:client) { Aws::ElasticBeanstalk::Client.new(stub_responses: responses) } + let(:s3) { Aws::S3::Client.new(stub_responses: true) } + let(:events) { [] } - before do - Aws.config[:s3] = { - stub_responses: { - } - } - Aws.config[:elasticbeanstalk] = { - stub_responses: { - create_application_version: ->(ctx) { - requests[:create_application_version] << ctx.http_request - { - application_version: { - version_label: 'label' - } - } - }, - update_environment: ->(ctx) { - requests[:update_environment] << ctx.http_request - }, - describe_environments: { - environments: [ - status: 'Ready' - ] - }, - describe_events: { - events: events + let(:responses) do + { + create_application_version: { + application_version: { + version_label: 'label' } + }, + update_environment: { + }, + describe_environments: { + environments: [ + status: 'Ready' + ] + }, + describe_events: { + events: events } } end - after { Aws.config.clear } - file 'one' file 'two' + before { allow(Aws::ElasticBeanstalk::Client).to receive(:new).and_return(client) } + before { allow(Aws::S3::Client).to receive(:new).and_return(s3) } + before { |c| subject.run unless c.example_group.metadata[:run].is_a?(FalseClass) } + describe 'by default' do before { subject.run } it { should have_zipped "travis-sha-#{now.to_i}.zip", %w(one two) } it { should have_run '[info] Using Access Key: i*******************' } - it { should create_app_version with: 'ApplicationName=dpl' } - it { should create_app_version with: 'Description=commit%20msg' } - it { should create_app_version with: 'S3Bucket=bucket' } - it { should create_app_version with: /S3Key=travis-sha-.*.zip/ } - it { should create_app_version with: /VersionLabel=travis-sha.*/ } + it { should create_app_version 'ApplicationName=dpl' } + it { should create_app_version 'Description=commit%20msg' } + it { should create_app_version 'S3Bucket=bucket' } + it { should create_app_version /S3Key=travis-sha-.*.zip/ } + it { should create_app_version /VersionLabel=travis-sha.*/ } it { should update_environment } end describe 'given --bucket_path one/two' do before { subject.run } - it { should create_app_version with: /S3Key=one%2Ftwo%2Ftravis-sha-.*.zip/ } + it { should create_app_version /S3Key=one%2Ftwo%2Ftravis-sha-.*.zip/ } end describe 'given --only_create_app_version' do @@ -80,21 +62,21 @@ describe 'given --zip_file other.zip' do before { subject.run } it { expect(File.exist?('other.zip')).to be true } - it { should create_app_version with: /S3Key=travis-sha-.*.zip/ } + it { should create_app_version /S3Key=travis-sha-.*.zip/ } end - describe 'given --wait_until_deployed' do + describe 'given --wait_until_deployed', run: false do let(:events) { [event_date: Time.now, severity: 'ERROR', message: 'msg'] } it { expect { subject.run }.to raise_error /Deployment failed/ } end - describe 'with an .ebignore file' do + describe 'with an .ebignore file', run: false do file '.ebignore', "*\n!one" before { subject.run } it { should have_zipped "travis-sha-#{now.to_i}.zip", %w(one) } end - describe 'with ~/.aws/credentials' do + describe 'with ~/.aws/credentials', run: false do let(:args) { |e| %w(--env env --bucket_name bucket) } file '~/.aws/credentials', <<-str.sub(/^\s*/, '') @@ -107,7 +89,7 @@ it { should have_run '[info] Using Access Key: ac******************' } end - describe 'with ~/.aws/config' do + describe 'with ~/.aws/config', run: false do let(:args) { |e| %w(--access_key_id id --secret_access_key secret) } file '~/.aws/config', <<-str.sub(/^\s*/, '') @@ -117,6 +99,6 @@ str before { subject.run } - it { should create_app_version with: 'S3Bucket=bucket' } + it { should create_app_version 'S3Bucket=bucket' } end end diff --git a/spec/dpl/providers/lambda_spec.rb b/spec/dpl/providers/lambda_spec.rb index 3fd51ec7d..1217c2710 100644 --- a/spec/dpl/providers/lambda_spec.rb +++ b/spec/dpl/providers/lambda_spec.rb @@ -1,44 +1,22 @@ describe Dpl::Providers::Lambda do - let(:args) { |e| required + args_from_description(e) } - let(:requests) { Hash.new { |hash, key| hash[key] = [] } } - let(:exists) { false } - - matcher :have_called do |key, params = {}| - match do |*| - @body = symbolize(JSON.parse(requests[key][0].body.read)) if requests[key].any? - @body ? expect(@body).to(include params) : false - end - - failure_message do - "Expected it to have called #{key.inspect}\n\n #{params.inspect}\n\nbut it was not. Instead it was called with:\n\n #{@body}" - end - end - - before do - Aws.config[:lambda] = { - stub_responses: { - get_function: ->(ctx) { - requests[:get_function] << ctx.http_request - exists ? {} : raise(Aws::Lambda::Errors::ResourceNotFoundException.new(ctx, 'error')) - }, - create_function: ->(ctx) { - requests[:create_function] << ctx.http_request - }, - update_function_configuration: ->(ctx) { - requests[:update_function_config] << ctx.http_request - { function_arn: 'arn' } - }, - update_function_code: ->(ctx) { - requests[:update_function_code] << ctx.http_request - }, - tag_resource: ->(ctx) { - requests[:tag_resource] << ctx.http_request - } - } + include Support::Matchers::Aws + + let(:args) { |e| required + args_from_description(e) } + let(:client) { Aws::Lambda::Client.new(stub_responses: responses) } + + let(:responses) do + { + get_function: ->(c) { + exists ? {} : raise(Aws::Lambda::Errors::ResourceNotFoundException.new(c, 'error')) + }, + create_function: {}, + update_function_configuration: { function_arn: 'arn' }, + update_function_code: {}, + tag_resource: {} } end - after { Aws.config.clear } + before { allow(Aws::Lambda::Client).to receive(:new).and_return(client) } file 'one' @@ -55,67 +33,67 @@ it { should have_run '[info] Creating function func.' } it { should have_run_in_order } - it { should have_called :create_function, FunctionName: 'func' } - it { should have_called :create_function, Runtime: 'nodejs8.10' } - it { should have_called :create_function, Code: { ZipFile: instance_of(String) } } - it { should have_called :create_function, Description: 'Deploy build 1 to AWS Lambda via Travis CI' } - it { should have_called :create_function, Handler: 'index.handler' } - it { should have_called :create_function, Role: 'role' } - it { should have_called :create_function, Timeout: 3 } - it { should have_called :create_function, MemorySize: 128 } - it { should have_called :create_function, TracingConfig: { Mode: 'PassThrough' } } + it { should create_function FunctionName: 'func' } + it { should create_function Runtime: 'nodejs8.10' } + it { should create_function Code: { ZipFile: instance_of(String) } } + it { should create_function Description: 'Deploy build 1 to AWS Lambda via Travis CI' } + it { should create_function Handler: 'index.handler' } + it { should create_function Role: 'role' } + it { should create_function Timeout: 3 } + it { should create_function MemorySize: 128 } + it { should create_function TracingConfig: { Mode: 'PassThrough' } } end describe 'given --module_name other --handler handler' do - it { should have_called :create_function, Handler: 'other.handler' } + it { should create_function Handler: 'other.handler' } end describe 'given --description other' do - it { should have_called :create_function, Description: 'other' } + it { should create_function Description: 'other' } end describe 'given --timeout 1' do - it { should have_called :create_function, Timeout: 1 } + it { should create_function Timeout: 1 } end describe 'given --memory_size 64' do - it { should have_called :create_function, MemorySize: 64 } + it { should create_function MemorySize: 64 } end describe 'given --runtime python2.7' do - it { should have_called :create_function, Runtime: 'python2.7' } + it { should create_function Runtime: 'python2.7' } end describe 'given --runtime java8' do - it { should have_called :create_function, Handler: 'index::handler' } + it { should create_function Handler: 'index::handler' } end describe 'given --subnet_ids one --subnet_ids two' do - it { should have_called :create_function, VpcConfig: { SubnetIds: ['one', 'two'] } } + it { should create_function VpcConfig: { SubnetIds: ['one', 'two'] } } end describe 'given --security_group_ids one --security_group_ids two' do - it { should have_called :create_function, VpcConfig: { SecurityGroupIds: ['one', 'two'] } } + it { should create_function VpcConfig: { SecurityGroupIds: ['one', 'two'] } } end describe 'given --dead_letter_arn arn' do - it { should have_called :create_function, DeadLetterConfig: { TargetArn: 'arn' } } + it { should create_function DeadLetterConfig: { TargetArn: 'arn' } } end describe 'given --tracing_mode Active' do - it { should have_called :create_function, TracingConfig: { Mode: 'Active' } } + it { should create_function TracingConfig: { Mode: 'Active' } } end describe 'given --environment_variables ONE=one --environment_variables TWO=two' do - it { should have_called :create_function, Environment: { Variables: { ONE: 'one', TWO: 'two' } } } + it { should create_function Environment: { Variables: { ONE: 'one', TWO: 'two' } } } end describe 'given --kms_key_arn arn' do - it { should have_called :create_function, KMSKeyArn: 'arn' } + it { should create_function KMSKeyArn: 'arn' } end describe 'given --function_tags key=value' do - it { should have_called :create_function, Tags: { key: 'value' } } + it { should create_function Tags: { key: 'value' } } end end @@ -130,76 +108,76 @@ it { should have_run '[info] Updating existing function func.' } it { should have_run '[info] Updating code.' } - it { should have_called :update_function_config, Runtime: 'nodejs8.10' } - it { should have_called :update_function_config, Description: 'Deploy build 1 to AWS Lambda via Travis CI' } - it { should have_called :update_function_config, Timeout: 3 } - it { should have_called :update_function_config, MemorySize: 128 } - it { should have_called :update_function_config, TracingConfig: { Mode: 'PassThrough' } } - it { should have_called :update_function_code, ZipFile: kind_of(String), Publish: false } + it { should update_function_config Runtime: 'nodejs8.10' } + it { should update_function_config Description: 'Deploy build 1 to AWS Lambda via Travis CI' } + it { should update_function_config Timeout: 3 } + it { should update_function_config MemorySize: 128 } + it { should update_function_config TracingConfig: { Mode: 'PassThrough' } } + it { should update_function_code ZipFile: instance_of(String), Publish: false } end describe 'given --role role' do - it { should have_called :update_function_config, Role: 'role' } + it { should update_function_config Role: 'role' } end describe 'given --handler_name handler' do - it { should have_called :update_function_config, Handler: 'index.handler' } + it { should update_function_config Handler: 'index.handler' } end describe 'given --module_name other --handler_name handler' do - it { should have_called :update_function_config, Handler: 'other.handler' } + it { should update_function_config Handler: 'other.handler' } end describe 'given --description other' do - it { should have_called :update_function_config, Description: 'other' } + it { should update_function_config Description: 'other' } end describe 'given --timeout 1' do - it { should have_called :update_function_config, Timeout: 1 } + it { should update_function_config Timeout: 1 } end describe 'given --memory_size 64' do - it { should have_called :update_function_config, MemorySize: 64 } + it { should update_function_config MemorySize: 64 } end describe 'given --runtime python2.7' do - it { should have_called :update_function_config, Runtime: 'python2.7' } + it { should update_function_config Runtime: 'python2.7' } end describe 'given --subnet_ids one --subnet_ids two' do - it { should have_called :update_function_config, VpcConfig: { SubnetIds: ['one', 'two'] } } + it { should update_function_config VpcConfig: { SubnetIds: ['one', 'two'] } } end describe 'given --security_group_ids one --security_group_ids two' do - it { should have_called :update_function_config, VpcConfig: { SecurityGroupIds: ['one', 'two'] } } + it { should update_function_config VpcConfig: { SecurityGroupIds: ['one', 'two'] } } end describe 'given --dead_letter_arn arn' do - it { should have_called :update_function_config, DeadLetterConfig: { TargetArn: 'arn' } } + it { should update_function_config DeadLetterConfig: { TargetArn: 'arn' } } end describe 'given --tracing_mode Active' do - it { should have_called :update_function_config, TracingConfig: { Mode: 'Active' } } + it { should update_function_config TracingConfig: { Mode: 'Active' } } end describe 'given --environment_variables ONE=one --environment_variables TWO=two' do - it { should have_called :update_function_config, Environment: { Variables: { ONE: 'one', TWO: 'two' } } } + it { should update_function_config Environment: { Variables: { ONE: 'one', TWO: 'two' } } } end describe 'given --kms_key_arn arn' do - it { should have_called :update_function_config, KMSKeyArn: 'arn' } + it { should update_function_config KMSKeyArn: 'arn' } end describe 'given --publish' do - it { should have_called :update_function_code, Publish: true } + it { should update_function_code Publish: true } end describe 'given --function_tags key=value' do - it { should have_called :tag_resource, Tags: { key: 'value' } } + it { should tag_resource Tags: { key: 'value' } } end describe 'given --layers one --layers two' do - it { should have_called :update_function_config, Layers: %w(one two) } + it { should update_function_config Layers: %w(one two) } end end @@ -219,6 +197,7 @@ describe 'with ~/.aws/config' do let(:args) { |e| %w(--access_key_id id --secret_access_key secret) } + let(:exists) { false } file '~/.aws/config', <<-str.sub(/^\s*/, '') [default] @@ -228,8 +207,8 @@ str before { subject.run } - it { should have_called :create_function, FunctionName: 'func' } - it { should have_called :create_function, Role: 'role' } - it { should have_called :create_function, Handler: 'index.handler' } + it { should create_function FunctionName: 'func' } + it { should create_function Role: 'role' } + it { should create_function Handler: 'index.handler' } end end diff --git a/spec/dpl/providers/opsworks_spec.rb b/spec/dpl/providers/opsworks_spec.rb index 781945fcd..f75db1e6e 100644 --- a/spec/dpl/providers/opsworks_spec.rb +++ b/spec/dpl/providers/opsworks_spec.rb @@ -1,92 +1,70 @@ describe Dpl::Providers::Opsworks do - let(:args) { |e| %w(--access_key_id access_key_id --secret_access_key secret_access_key --app_id app) + args_from_description(e) } - let(:requests) { Hash.new { |hash, key| hash[key] = [] } } - - matcher :have_called do |key, params = {}| - match do |*| - @body = symbolize(JSON.parse(requests[key][0].body.read)) if requests[key].any? - @body ? expect(@body).to(include(params)) : false - end - - failure_message do - "Expected it to have called #{key.inspect}\n\n #{params.inspect}\n\nbut it was not. Instead it was called with:\n\n #{@body}" - end - end - - before do - Aws.config[:opsworks] = { - stub_responses: { - describe_apps: ->(ctx) { - requests[:describe_apps] << ctx.http_request - { apps: [stack_id: 'stack_id', shortname: 'dpl'] } - }, - create_deployment: ->(ctx) { - requests[:create_deployment] << ctx.http_request - { deployment_id: 'id' } - }, - describe_deployments: ->(ctx) { - requests[:describe_deployments] << ctx.http_request - { deployments: [status: 'successful'] } - }, - update_app: ->(ctx) { - requests[:update_app] << ctx.http_request - } + include Support::Matchers::Aws + + let(:args) { |e| %w(--access_key_id access_key_id --secret_access_key secret_access_key --app_id app) + args_from_description(e) } + let(:client) { Aws::OpsWorks::Client.new(stub_responses: responses) } + + let(:responses) do + { + describe_apps: { + apps: [stack_id: 'stack_id', shortname: 'dpl'] + }, + create_deployment: { + deployment_id: 'id' + }, + describe_deployments: { + deployments: [status: 'successful'] + }, + update_app: { } } end - after { Aws.config.clear } + before { allow(Aws::OpsWorks::Client).to receive(:new).and_return(client) } + before { |c| subject.run unless c.example_group.metadata[:run].is_a?(FalseClass) } - context do - before { subject.run } + describe 'by default', record: true do + let(:json) { JSON.dump(deploy: { dpl: { migrate: false, scm: { revision: 'sha' } } }) } + + it { should have_run '[info] Using Access Key: ac******************' } + it { should have_run '[print] Creating deployment ... ' } + it { should have_run '[info] Done: id' } + it { should have_run_in_order } + + it { should create_deployment StackId: 'stack_id' } + it { should create_deployment AppId: 'app' } + it { should create_deployment Command: { Name: 'deploy' } } + it { should create_deployment Comment: 'Deploy build 1 via Travis CI' } + it { should create_deployment CustomJson: json } + end + + describe 'given --instance_ids one --instance_ids two' do + it { should create_deployment InstanceIds: ['one', 'two'] } + end + + describe 'given --layer_ids one --layer_ids two' do + it { should create_deployment LayerIds: ['one', 'two'] } + end + + describe 'given --migrate' do + let(:json) { JSON.dump(deploy: { dpl: { migrate: true, scm: { revision: 'sha' } } }) } + it { should create_deployment CustomJson: json } + end + + describe 'given --custom_json danger:will-robinson' do + it { should create_deployment CustomJson: 'danger:will-robinson' } + end + + describe 'given --wait_until_deployed' do + it { should have_run '[print] Deploying ' } + it { should describe_deployments DeploymentIds: ['id'] } + end - describe 'by default', record: true do - let(:json) { JSON.dump(deploy: { dpl: { migrate: false, scm: { revision: 'sha' } } }) } - - it { should have_run '[info] Using Access Key: ac******************' } - it { should have_run '[print] Creating deployment ... ' } - it { should have_run '[info] Done: id' } - it { should have_run_in_order } - - it do - should have_called :create_deployment, { - StackId: 'stack_id', - AppId: 'app', - Command: { Name: 'deploy' }, - Comment: 'Deploy build 1 via Travis CI', - CustomJson: json - } - end - end - - describe 'given --instance_ids one --instance_ids two' do - it { should have_called :create_deployment, InstanceIds: ['one', 'two'] } - end - - describe 'given --layer_ids one --layer_ids two' do - it { should have_called :create_deployment, LayerIds: ['one', 'two'] } - end - - describe 'given --migrate' do - let(:json) { JSON.dump(deploy: { dpl: { migrate: true, scm: { revision: 'sha' } } }) } - it { should have_called :create_deployment, CustomJson: json } - end - - describe 'given --custom_json danger:will-robinson' do - it { should have_called :create_deployment, CustomJson: 'danger:will-robinson' } - end - - describe 'given --wait_until_deployed' do - it { should have_run '[print] Deploying ' } - it { should have_called :describe_deployments, DeploymentIds: ['id'] } - end - - describe 'given --wait_until_deployed --update_on_success' do - it { should have_called :update_app, AppId: 'app', AppSource: { Revision: 'sha' } } - end + describe 'given --wait_until_deployed --update_on_success' do + it { should update_app AppId: 'app', AppSource: { Revision: 'sha' } } end - describe 'with ~/.aws/credentials' do + describe 'with ~/.aws/credentials', run: false do let(:args) { |e| %w(--app_id app) } let(:exists) { false } @@ -100,7 +78,7 @@ it { should have_run '[info] Using Access Key: ac******************' } end - describe 'with ~/.aws/config' do + describe 'with ~/.aws/config', run: false do let(:args) { |e| %w(--access_key_id id --secret_access_key secret) } file '~/.aws/config', <<-str.sub(/^\s*/, '') @@ -109,6 +87,6 @@ str before { subject.run } - it { should have_called :create_deployment, AppId: 'app' } + it { should create_deployment AppId: 'app' } end end diff --git a/spec/dpl/providers/s3_spec.rb b/spec/dpl/providers/s3_spec.rb index 449c1589e..fb7479e64 100644 --- a/spec/dpl/providers/s3_spec.rb +++ b/spec/dpl/providers/s3_spec.rb @@ -1,59 +1,31 @@ describe Dpl::Providers::S3 do - let(:args) { |e| %w(--access_key_id access_key_id --secret_access_key secret_access_key --bucket bucket) + args_from_description(e) } - let(:requests) { Hash.new { |hash, key| hash[key] = [] } } + include Support::Matchers::Aws + + let(:args) { |e| %w(--access_key_id access_key_id --secret_access_key secret_access_key --bucket bucket) + args_from_description(e) } + let(:client) { Aws::S3::Client.new(stub_responses: {}) } file 'one.txt' file '.hidden.txt' - before do - Aws.config[:s3] = { - stub_responses: { - put_object: ->(ctx) { - requests[:put_object] << ctx.http_request - }, - put_bucket_website: ->(ctx) { - requests[:put_bucket_website] << ctx.http_request - } - } - } - end - - matcher :put_object do |file, opts = {}| - match do |*| - next false unless request = requests[:put_object].detect { |f| f.body.path == file } - path = opts.delete(:path) - return false if path && path != request.endpoint.path - host = opts.delete(:host) - return false if host && host != request.endpoint.host - headers = symbolize(request.headers.to_h) - expect(headers).to(include(opts)) if headers.any? - true - end - end - - matcher :put_bucket_website_suffix do |suffix| - match do |*| - next false unless request = requests[:put_bucket_website][0] - request.body.read.include?("#{suffix}") - end - end - - before { |c| subject.run unless c.metadata[:run].is_a?(FalseClass) } + before { allow(Aws::S3::Client).to receive(:new).and_return(client) } + before { |c| subject.run unless c.example_group.metadata[:run].is_a?(FalseClass) } describe 'by default', record: true do it { should have_run '[info] Using Access Key: ac******************' } it { should have_run '[info] Uploading 1 files with up to 5 threads ...' } it { should have_run '[print] .' } it { should have_run_in_order } - it { should put_object 'one.txt', host: 'bucket.s3.amazonaws.com', 'x-amz-acl': 'private', 'cache-control': 'no-cache', 'x-amz-storage-class': 'STANDARD' } + # for whatever reason host would either include `us-stubbed-1` or not + # depending if this spec is run as part of the full suite or alone. + it { should put_object 'one.txt', host: /bucket\.s3\.(us-stubbed-1\.)?amazonaws.com/, 'x-amz-acl': 'private', 'cache-control': 'no-cache', 'x-amz-storage-class': 'STANDARD' } end describe 'given --endpoint https://host.com' do - it { should put_object 'one.txt', host: 'bucket.host.com' } + it { should create_client endpoint: URI.parse('https://host.com') } end describe 'given --region us-west-1' do - it { should put_object 'one.txt', host: 'bucket.s3.us-west-1.amazonaws.com' } + it { should create_client region: 'us-west-1' } end describe 'given --upload_dir dir' do @@ -61,50 +33,47 @@ end describe 'given --dot_match' do + it { should have_run %r(.hidden.txt) } it { should put_object '.hidden.txt' } end describe 'given --storage_class STANDARD_IA' do + it { should have_run %r(one.txt.* storage_class=STANDARD_IA) } it { should put_object 'one.txt', 'x-amz-storage-class': 'STANDARD_IA' } end describe 'given --acl public_read' do + it { should have_run %r(one.txt.* acl=public-read) } it { should put_object 'one.txt', 'x-amz-acl': 'public-read' } end describe 'given --cache_control public' do + it { should have_run %r(one.txt.* cache_control=public) } it { should put_object 'one.txt', 'cache-control': 'public' } end describe 'given --cache_control max-age=60' do + it { should have_run %r(one.txt.* cache_control=max-age=60) } it { should put_object 'one.txt', 'cache-control': 'max-age=60' } end - describe 'given --cache_control "public: *.js" --cache_control "no-cache: *.txt"' do - it { should put_object 'one.txt', 'cache-control': 'no-cache' } - end - - describe 'given --cache_control "public: *.js" --cache_control "private"' do - it { should put_object 'one.txt', 'cache-control': 'private' } - end - describe 'given --expires "2020-01-01 00:00:00 UTC"' do - it { should put_object 'one.txt', 'expires': 'Wed, 01 Jan 2020 00:00:00 GMT' } - end - - describe 'given --expires "2020-01-01 00:00:00 UTC: *.txt, *.js"' do + it { should have_run %r(one.txt.* expires=2020-01-01 00:00:00 UTC) } it { should put_object 'one.txt', 'expires': 'Wed, 01 Jan 2020 00:00:00 GMT' } end describe 'given --default_text_charset utf-8' do + it { should have_run %r(one.txt.* content_type=text/plain; charset=utf-8) } it { should put_object 'one.txt', 'content-type': 'text/plain; charset=utf-8' } end describe 'given --server_side_encryption' do + it { should have_run %r(one.txt.* server_side_encryption=AES256) } it { should put_object 'one.txt', 'x-amz-server-side-encryption': 'AES256' } end describe 'given --detect_encoding' do + it { should have_run %r(one.txt.* content_encoding=text) } it { should put_object 'one.txt', 'content-encoding': 'text' } end @@ -150,12 +119,13 @@ file '~/.aws/config', <<-str.sub(/^\s*/, '') [default] - region=us-west-1 + region=us-other-1 bucket=other str before { subject.run } - it { should put_object 'one.txt', host: 'other.s3.us-west-1.amazonaws.com' } + it { should create_client region: 'us-other-1' } + it { should put_object 'one.txt', host: /other\.s3\.(us-stubbed-1\.)?amazonaws.com/ } end describe 'Mapping', run: false do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 28d4ea356..c1a4df9f7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,6 +11,7 @@ c.include Support::Env c.include Support::File c.include Support::Fixtures + c.include Support::Helpers c.include Support::Matchers c.include Support::Matchers::RecordCmds, record: true c.include Support::Now diff --git a/spec/support.rb b/spec/support.rb index a0b78f7ff..519447d20 100644 --- a/spec/support.rb +++ b/spec/support.rb @@ -4,22 +4,7 @@ require 'support/file' require 'support/fixtures' require 'support/gemfile' +require 'support/helpers' require 'support/matchers' require 'support/now' require 'support/require' - -def stringify(obj) - case obj - when Hash then obj.map { |key, obj| [key.to_s, symbolize(obj)] }.to_h - when Array then obj.map { |obj| symbolize(obj) } - else obj - end -end - -def symbolize(obj) - case obj - when Hash then obj.map { |key, obj| [key.to_sym, symbolize(obj)] }.to_h - when Array then obj.map { |obj| symbolize(obj) } - else obj - end -end diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb new file mode 100644 index 000000000..51028f0ca --- /dev/null +++ b/spec/support/helpers.rb @@ -0,0 +1,31 @@ +module Support + module Helpers + def only(hash, *keys) + hash.select { |key, _| keys.include?(key) }.to_h + end + + def except(hash, *keys) + hash.reject { |key, _| keys.include?(key) }.to_h + end + + def compact(hash) + hash.reject { |_, value| value.nil? } + end + + def stringify(obj) + case obj + when Hash then obj.map { |key, obj| [key.to_s, stringify(obj)] }.to_h + when Array then obj.map { |obj| stringify(obj) } + else obj + end + end + + def symbolize(obj) + case obj + when Hash then obj.map { |key, obj| [key.to_sym, symbolize(obj)] }.to_h + when Array then obj.map { |obj| symbolize(obj) } + else obj + end + end + end +end diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index ea5fe52d4..35d511d40 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -1,3 +1,4 @@ +require 'support/matchers/aws' require 'support/matchers/have_deprecated' require 'support/matchers/have_env' require 'support/matchers/have_logged' diff --git a/spec/support/matchers/aws.rb b/spec/support/matchers/aws.rb new file mode 100644 index 000000000..cbf27f412 --- /dev/null +++ b/spec/support/matchers/aws.rb @@ -0,0 +1,192 @@ +module Support + module Matchers + module Aws + class Base < Struct.new(:opts) + include Support::Helpers, RSpec::Matchers::BuiltIn, + RSpec::Mocks::Matchers, RSpec::Mocks::ArgumentMatchers + end + + class HaveRequested < Base + def matches?(*) + !!request + end + + def description + "have requested #{operation}" + end + + def failure_message + msg = "Expected the operation #{operation.inspect} to be requested#{" with #{format_opts}" if opts.any?}, but it wasn't." + msg << "\n\nInstead the following requests were made:\n\n #{format_requests}" if requests.any? + msg + end + + def failure_message_when_negated + "Expected the operation #{operation.inspect} to not be requested#{" with #{format_opts}" if opts.any?}, but it was." + end + + def request + host, path, body, file, headers = opts.values_at(:host, :path, :body, :file, :headers) + headers ||= except(opts, :host, :path, :body, :file, :client, :operation) + headers = stringify(headers) + reqs = requests.select { |r| r[:operation] == operation } + reqs = reqs.select { |r| match?(r[:host], host) } if host + reqs = reqs.select { |r| r[:path] == path } if path + reqs = reqs.select { |r| match?(r[:body], body) } if body + reqs = reqs.select { |r| r[:file] == file } if file + reqs = reqs.select { |r| include?(r[:headers], headers) } if headers + reqs.any? and reqs[0][:request] + end + + def requests + client.api_requests.map do |req| + compact( + operation: req[:operation_name], + host: req[:context].http_request.endpoint.host, + path: req[:context].http_request.endpoint.path, + body: body_from(req[:context].http_request.body), + file: req[:params][:body] && req[:params][:body].path, + headers: req[:context].http_request.headers, + request: req[:context].http_request + ) + end + end + + def body_from(obj) + case obj + when StringIO + obj.string + when ::Aws::Query::ParamList::IoWrapper + obj.read + end + end + + def format_opts + except(opts, :client, :operation).map { |pair| pair.join('=') }.join(' ') + end + + def format_requests + requests.map { |r| except(r, :request).map { |key, value| [key, value.inspect].join(': ') }.join("\n ") }.join("\n\n") + end + + def match?(actual, expected) + case expected + when String then actual.include?(expected) + when Regexp then actual =~ expected + when Hash then include?(JSON.parse(actual), stringify(expected)) + end + end + + def include?(hash, other) + # Include.new(other).matches?(hash) + other.all? do |key, value| + case value + when RSpec::Mocks::ArgumentMatchers::InstanceOf + # just can't get this working in any other way ... + hash[key].is_a?(value.instance_variable_get(:@klass)) + when Hash + include?(hash[key], value) + else + hash[key] == value + end + end + end + + def client + opts[:client] + end + + def operation + opts[:operation] + end + end + + class CreateClient < Base + def matches?(*) + matcher.matches?(::Aws::S3::Client) + end + + def description + matcher.description + end + + def failure_message + matcher.failure_message + end + + def failure_message_when_negated + matcher.failure_message_when_negated + end + + def matcher + @matchers ||= HaveReceived.new(:new).with(hash_including(opts)) + end + end + + def create_client(opts) + CreateClient.new(opts) + end + + def have_requested(operation, opts = {}) + HaveRequested.new(opts.merge(client: client, operation: operation)) + end + + # elasticbeanstalk + + def create_app_version(body = nil) + have_requested(:create_application_version, compact(body: body)) + end + + def update_environment + have_requested(:update_environment) + end + + # lambda + + def create_function(params) + have_requested(:create_function, body: params) + end + + def update_function_config(params) + have_requested(:update_function_configuration, body: params) + end + + def update_function_code(params) + have_requested(:update_function_code, body: params) + end + + def tag_resource(params) + have_requested(:tag_resource, body: params) + end + + # codedeploy and opsworks + + def create_deployment(params) + have_requested(:create_deployment, body: params) + end + + def get_deployment + have_requested(:get_deployment) + end + + def describe_deployments(params) + have_requested(:describe_deployments, body: params) + end + + def update_app(params) + have_requested(:update_app, body: params) + end + + # s3 + + def put_object(file, opts = {}) + have_requested(:put_object, opts.merge(file: file)) + end + + def put_bucket_website_suffix(suffix) + have_requested(:put_bucket_website, body: %r(#{suffix})) + end + end + end +end +