diff --git a/CHANGES.md b/CHANGES.md index 37d47f3..b9d7c15 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # CHANGES +## 1.7.0 (2020-11-25) + +* Feature: --no-fail-on-empty-change-set + ## 1.6.0 (2020-11-19) * Feature: Support --service-role-arn on "change-set create" diff --git a/README.md b/README.md index 9805f3c..4cf7621 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,8 @@ The change-set name defaults to "pending", but can be overridden using `--name`. The `change-set create` subcommand, like the `up` command, supports `--service-role-arn` to specify a service role. +It is impossible to create a change set with no changes. By default, stackup will only return successfully if a change set was actually created, and will otherwise fail. If the `--no-fail-on-empty-change-set` option is provided, stackup will return successfully if a change set was created _or_ if no change set was created because no changes were needed. + ## Programmatic usage Get a handle to a `Stack` object as follows: diff --git a/lib/stackup/change_set.rb b/lib/stackup/change_set.rb index bd9bcb7..62d6395 100644 --- a/lib/stackup/change_set.rb +++ b/lib/stackup/change_set.rb @@ -56,6 +56,7 @@ def create(options = {}) options[:change_set_name] = name options[:change_set_type] = stack.exists? ? "UPDATE" : "CREATE" force = options.delete(:force) + allow_empty_change_set = options.delete(:allow_empty_change_set) options[:template_body] = MultiJson.dump(options.delete(:template)) if options[:template] # optionally override template_body with the original template to preserve formatting (& comments in YAML) template_orig = options.delete(:template_orig) @@ -73,8 +74,12 @@ def create(options = {}) when /COMPLETE/ return current.status when "FAILED" - logger.error(current.status_reason) - raise StackUpdateError, "change-set creation failed" if status == "FAILED" + if allow_empty_change_set and current.status_reason == "The submitted information didn't contain changes. Submit different information to create a change set." + return current.status_reason + else + logger.error(current.status_reason) + raise StackUpdateError, "change-set creation failed" if status == "FAILED" + end end sleep(wait_poll_interval) end diff --git a/lib/stackup/main_command.rb b/lib/stackup/main_command.rb index 35c9a1b..56c97ed 100644 --- a/lib/stackup/main_command.rb +++ b/lib/stackup/main_command.rb @@ -280,6 +280,9 @@ def pad(s, width) option ["--force"], :flag, "replace existing change-set of the same name" + option ["--no-fail-on-empty-change-set"], :flag, "don't fail on empty change-set", + :attribute_name => :allow_empty_change_set + include HasParameters option "--tags", "FILE", "stack tags file", @@ -314,6 +317,7 @@ def execute options[:role_arn] = service_role_arn if service_role_arn options[:use_previous_template] = use_previous_template? options[:force] = force? + options[:allow_empty_change_set] = allow_empty_change_set? options[:capabilities] = capability_list options[:preserve] = preserve_template_formatting? report_change do diff --git a/lib/stackup/version.rb b/lib/stackup/version.rb index 8f66a8b..3ed1507 100644 --- a/lib/stackup/version.rb +++ b/lib/stackup/version.rb @@ -1,5 +1,5 @@ module Stackup - VERSION = "1.6.0".freeze + VERSION = "1.7.0".freeze end diff --git a/spec/stackup/main_command_spec.rb b/spec/stackup/main_command_spec.rb index 1dc6c08..f05c118 100644 --- a/spec/stackup/main_command_spec.rb +++ b/spec/stackup/main_command_spec.rb @@ -1,15 +1,19 @@ require "stackup/main_command" describe Stackup::MainCommand do - context "change-set create --service-role-arn" - it "invokes stack.change_set.create with role arn passed through" do - mock_stackup = double() - mock_stack = double() - mock_change_set = double() - allow_any_instance_of(Stackup::MainCommand).to receive(:Stackup).and_return(mock_stackup) - allow(mock_stackup).to receive(:stack).and_return(mock_stack) - allow(mock_stack).to receive(:change_set).and_return(mock_change_set) + let(:mock_change_set) { double() } + + before(:example) do + mock_stackup = double() + mock_stack = double() + allow_any_instance_of(Stackup::MainCommand).to receive(:Stackup).and_return(mock_stackup) + allow(mock_stackup).to receive(:stack).and_return(mock_stack) + allow(mock_stack).to receive(:change_set).and_return(mock_change_set) + end + + context "change-set create --service-role-arn" do + it "invokes stack.change_set.create with role arn passed through" do expected_args = { role_arn: "arn:aws:iam::000000000000:role/example" } @@ -20,4 +24,33 @@ "--template", "examples/template.yml", "--service-role-arn", "arn:aws:iam::000000000000:role/example"]) end + end + + context "change-set create" do + it "invokes stack.change_set.create with allow_empty_change_set nil" do + expected_args = { + allow_empty_change_set: nil + } + expect(mock_change_set).to receive(:create).with(hash_including(expected_args)) + + Stackup::MainCommand.run("stackup", [ + "STACK-NAME", "change-set", "create", + "--template", "examples/template.yml"]) + end + end + + context "change-set create --no-fail-on-empty-change-set" do + it "invokes stack.change_set.create with allow_empty_change_set true" do + expected_args = { + allow_empty_change_set: true + } + expect(mock_change_set).to receive(:create).with(hash_including(expected_args)) + + Stackup::MainCommand.run("stackup", [ + "STACK-NAME", "change-set", "create", + "--template", "examples/template.yml", + "--no-fail-on-empty-change-set"]) + end + end + end diff --git a/spec/stackup/stack_spec.rb b/spec/stackup/stack_spec.rb index aef5258..8f491bc 100644 --- a/spec/stackup/stack_spec.rb +++ b/spec/stackup/stack_spec.rb @@ -429,6 +429,38 @@ def create_change_set end + context "when allow_empty_change_set is nil and there are no changes" do + it "raises an exception" do + cf_client.stub_responses(:describe_change_set, [{ + status: "FAILED", + status_reason: "The submitted information didn't contain changes. Submit different information to create a change set." + }]) + expect { create_change_set }.to raise_error(Stackup::StackUpdateError) + end + end + + context "when allow_empty_change_set is true and there are no changes" do + it "does not raise an exception" do + cf_client.stub_responses(:describe_change_set, [{ + status: "FAILED", + status_reason: "The submitted information didn't contain changes. Submit different information to create a change set." + }]) + options[:allow_empty_change_set] = true + expect { create_change_set }.not_to raise_error + end + end + + context "when allow_empty_change_set is true and there is some other failure" do + it "raises an exception" do + cf_client.stub_responses(:describe_change_set, [{ + status: "FAILED", + status_reason: "some other failure message" + }]) + options[:allow_empty_change_set] = true + expect { create_change_set }.to raise_error(Stackup::StackUpdateError) + end + end + end describe "#change_set#execute" do