diff --git a/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema.rb b/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema.rb index 361444bbb..c0a3f2c3b 100644 --- a/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema.rb +++ b/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema.rb @@ -30,6 +30,8 @@ class VerifiablePactsJSONQuerySchema optional(:latest).filled(included_in?: [true, false]) optional(:fallbackTag).filled(:str?) optional(:consumer).filled(:str?, :not_blank?) + optional(:currentlyDeployed).filled(included_in?: [true]) + optional(:environment).filled(:str?) # rule(fallbackTagMustBeForLatest: [:fallbackTag, :latest]) do | fallback_tag, latest | # fallback_tag.filled?.then(latest.eql?(true)) diff --git a/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb b/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb index a57349610..065bd75ae 100644 --- a/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb +++ b/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb @@ -23,6 +23,8 @@ class VerifiablePactsQueryDecorator < BaseDecorator } property :fallback_tag property :consumer + property :environment + property :currently_deployed end property :include_pending_status, default: false, diff --git a/lib/pact_broker/pacts/pact_publication_dataset_module.rb b/lib/pact_broker/pacts/pact_publication_dataset_module.rb index de5eab08c..faa4b2061 100644 --- a/lib/pact_broker/pacts/pact_publication_dataset_module.rb +++ b/lib/pact_broker/pacts/pact_publication_dataset_module.rb @@ -16,6 +16,7 @@ def for_provider_and_consumer_version_selector provider, selector # Do this last so that the provider/consumer criteria get included in the "latest" query before the join, rather than after query = query.latest_for_consumer_branch(selector.branch) if selector.latest_for_branch? query = query.latest_for_consumer_tag(selector.tag) if selector.latest_for_tag? + query = query.for_currently_deployed_versions(selector.environment) if selector.currently_deployed? query = query.overall_latest if selector.overall_latest? query end @@ -149,6 +150,23 @@ def latest_for_consumer_tag(tag_name) .remove_overridden_revisions_from_complete_query end + def for_currently_deployed_versions(environment_name) + deployed_versions_join = { + Sequel[:pact_publications][:consumer_version_id] => Sequel[:deployed_versions][:version_id], + Sequel[:deployed_versions][:currently_deployed] => true + } + environments_join = { + Sequel[:deployed_versions][:environment_id] => Sequel[:environments][:id], + Sequel[:environments][:name] => environment_name + }.compact + + query = self + if no_columns_selected? + query = query.select_all_qualified.select_append(Sequel[:environments][:name].as(:environment_name)) + end + query.join(:deployed_versions, deployed_versions_join).join(:environments, environments_join) + end + def successfully_verified_by_provider_branch(provider_id, provider_version_branch) verifications_join = { pact_version_id: :pact_version_id, diff --git a/lib/pact_broker/pacts/pacts_for_verification_repository.rb b/lib/pact_broker/pacts/pacts_for_verification_repository.rb index 9372894dd..7041512c0 100644 --- a/lib/pact_broker/pacts/pacts_for_verification_repository.rb +++ b/lib/pact_broker/pacts/pacts_for_verification_repository.rb @@ -17,7 +17,7 @@ class PactsForVerificationRepository include PactBroker::Repositories::Helpers def find(provider_name, consumer_version_selectors) - selected_pacts = find_pacts_for_which_the_latest_version_of_something_is_required(provider_name, consumer_version_selectors) + + selected_pacts = find_pacts_by_selector(provider_name, consumer_version_selectors) + find_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors) selected_pacts = selected_pacts + find_pacts_for_fallback_tags(selected_pacts, provider_name, consumer_version_selectors) merge_selected_pacts(selected_pacts) @@ -101,7 +101,7 @@ def find_pacts_for_fallback_tags(selected_pacts, provider_name, consumer_version end end - def find_pacts_for_which_the_latest_version_of_something_is_required(provider_name, consumer_version_selectors) + def find_pacts_by_selector(provider_name, consumer_version_selectors) provider = pacticipant_repository.find_by_name(provider_name) selectors = if consumer_version_selectors.empty? @@ -109,15 +109,21 @@ def find_pacts_for_which_the_latest_version_of_something_is_required(provider_na else consumer_version_selectors.select(&:latest_for_tag?) + consumer_version_selectors.select(&:latest_for_branch?) + - consumer_version_selectors.select(&:overall_latest?) + consumer_version_selectors.select(&:overall_latest?) + + consumer_version_selectors.select(&:currently_deployed?) end selectors.flat_map do | selector | query = scope_for(PactPublication).for_provider_and_consumer_version_selector(provider, selector) query.all.collect do | pact_publication | + resolved_selector = if selector.currently_deployed? + selector.resolve_for_environment(pact_publication.consumer_version, pact_publication.values.fetch(:environment_name)) + else + selector.resolve(pact_publication.consumer_version) + end SelectedPact.new( pact_publication.to_domain, - Selectors.new(selector.resolve(pact_publication.consumer_version)) + Selectors.new(resolved_selector) ) end end @@ -141,6 +147,7 @@ def find_pacts_for_which_the_latest_version_for_the_fallback_tag_is_required(pro def find_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors) # The tags for which all versions are specified + # Need to move support for this into PactPublication.for_provider_and_consumer_version_selector selectors = consumer_version_selectors.select(&:all_for_tag?) selectors.flat_map do | selector | diff --git a/lib/pact_broker/pacts/selector.rb b/lib/pact_broker/pacts/selector.rb index 640498d51..fc8380755 100644 --- a/lib/pact_broker/pacts/selector.rb +++ b/lib/pact_broker/pacts/selector.rb @@ -17,6 +17,28 @@ def resolve_for_fallback(consumer_version) ResolvedSelector.new(self.to_h, consumer_version) end + def resolve_for_environment(consumer_version, environment) + ResolvedSelector.new(self.to_h.merge(environment: environment), consumer_version) + end + + # Only currently used to identify the currently_deployed from the others in + # verifiable_pact_messages, so don't need the "for_consumer" sub category + def type + if latest_for_branch? + :latest_for_branch + elsif currently_deployed? + :currently_deployed + elsif latest_for_tag? + :latest_for_tag + elsif all_for_tag? + :all_for_tag + elsif overall_latest? + :overall_latest + else + :undefined + end + end + def tag= tag self[:tag] = tag end @@ -57,6 +79,26 @@ def consumer self[:consumer] end + def currently_deployed= currently_deployed + self[:currently_deployed] = currently_deployed + end + + def currently_deployed + self[:currently_deployed] + end + + def currently_deployed? + currently_deployed + end + + def environment= environment + self[:environment] = environment + end + + def environment + self[:environment] + end + def self.overall_latest Selector.new(latest: true) end @@ -97,6 +139,18 @@ def self.latest_for_consumer(consumer) Selector.new(latest: true, consumer: consumer) end + def self.for_currently_deployed(environment = nil) + Selector.new( { currently_deployed: true, environment: environment }.compact ) + end + + def self.for_currently_deployed_and_consumer(consumer) + Selector.new(currently_deployed: true, consumer: consumer) + end + + def self.for_currently_deployed_and_environment_and_consumer(environment, consumer) + Selector.new(currently_deployed: true, environment: environment, consumer: consumer) + end + def self.from_hash hash Selector.new(hash) end @@ -118,7 +172,7 @@ def branch end def overall_latest? - !!(latest? && !tag && !branch) + !!(latest? && !tag && !branch && !currently_deployed && !environment) end # Not sure if the fallback_tag logic is needed @@ -164,6 +218,12 @@ def <=> other else latest_for_branch? ? -1 : 1 end + elsif currently_deployed? || other.currently_deployed? + if currently_deployed? == other.currently_deployed? + environment <=> other.environment + else + currently_deployed? ? -1 : 1 + end elsif latest_for_tag? || other.latest_for_tag? if latest_for_tag? == other.latest_for_tag? tag <=> other.tag diff --git a/lib/pact_broker/pacts/verifiable_pact_messages.rb b/lib/pact_broker/pacts/verifiable_pact_messages.rb index 634693aff..e96d7fd74 100644 --- a/lib/pact_broker/pacts/verifiable_pact_messages.rb +++ b/lib/pact_broker/pacts/verifiable_pact_messages.rb @@ -75,9 +75,12 @@ def pact_version_short_description attr_reader :verifiable_pact, :pact_version_url def join(list, last_joiner = " and ") - quoted_list = list.collect { | tag | "'#{tag}'" } - comma_joined = quoted_list[0..-3] || [] - and_joined = quoted_list[-2..-1] || quoted_list + join_unquoted(list.collect { | word | "'#{word}'" }, last_joiner = " and ") + end + + def join_unquoted(list, last_joiner = " and ") + comma_joined = list[0..-3] || [] + and_joined = list[-2..-1] || list if comma_joined.any? "#{comma_joined.join(', ')}, #{and_joined.join(last_joiner)}" else @@ -153,11 +156,24 @@ def branches end def selector_descriptions - selectors.sort.collect do | selector | - selector_description(selector) + selectors.sort.group_by(&:type).values.flat_map do | selectors | + selectors_descriptions(selectors) end.join(", ") end + def selectors_descriptions(selectors) + if selectors.first.currently_deployed? + selectors.group_by(&:consumer).flat_map do | consumer_name, selectors | + display_name = consumer_name ? "the version(s) of #{consumer_name}" : "the consumer version(s)" + "pacts for #{display_name} currently deployed to #{join_unquoted(selectors.collect(&:environment))}" + end + else + selectors.collect do | selector | + selector_description(selector) + end + end + end + def selector_description selector if selector.overall_latest? consumer_label = selector.consumer ? selector.consumer : 'a consumer' @@ -180,6 +196,8 @@ def selector_description selector "pacts for all #{selector.consumer} versions tagged '#{selector.tag}'" elsif selector.all_for_tag? "pacts for all consumer versions tagged '#{selector.tag}'" + elsif selector.currently_deployed? + "pacts for consumer version(s) currently deployed to #{selector.environment}" else selector.to_json end diff --git a/spec/lib/pact_broker/pacts/repository_find_for_currently_deployed_spec.rb b/spec/lib/pact_broker/pacts/repository_find_for_currently_deployed_spec.rb new file mode 100644 index 000000000..f6059cf85 --- /dev/null +++ b/spec/lib/pact_broker/pacts/repository_find_for_currently_deployed_spec.rb @@ -0,0 +1,124 @@ +require 'pact_broker/pacts/repository' + +module PactBroker + module Pacts + describe Repository do + describe "#find_for_verification" do + def find_by_consumer_version_number(consumer_version_number) + subject.find{ |pact| pact.consumer_version_number == consumer_version_number } + end + + def find_by_consumer_name_and_consumer_version_number(consumer_name, consumer_version_number) + subject.find{ |pact| pact.consumer_name == consumer_name && pact.consumer_version_number == consumer_version_number } + end + + subject { Repository.new.find_for_verification("Bar", consumer_version_selectors) } + + context "when currently_deployed is true" do + before do + td.create_environment("test") + .create_pact_with_hierarchy("Foo", "1", "Bar") + .create_deployed_version_for_consumer_version(currently_deployed: false) + .create_pact_with_hierarchy("Foo", "2", "Bar") + .create_deployed_version_for_consumer_version(currently_deployed: true) + .create_pact_with_hierarchy("Waffle", "3", "Bar") + .create_pact_with_hierarchy("Waffle", "4", "Bar") + .create_deployed_version_for_consumer_version(currently_deployed: true) + end + + let(:consumer_version_selectors) do + PactBroker::Pacts::Selectors.new( + PactBroker::Pacts::Selector.for_currently_deployed + ) + end + + it "returns the pacts for the currently deployed versions" do + expect(subject.size).to eq 2 + expect(subject.first.selectors).to eq [PactBroker::Pacts::Selector.for_currently_deployed.resolve_for_environment(td.find_version("Foo", "2"), "test")] + expect(subject.last.selectors).to eq [PactBroker::Pacts::Selector.for_currently_deployed.resolve_for_environment(td.find_version("Waffle", "4"), "test")] + end + end + + context "when currently_deployed is true and an environment is specified" do + before do + td.create_environment("test") + .create_pact_with_hierarchy("Foo", "1", "Bar") + .create_deployed_version_for_consumer_version(currently_deployed: false) + .create_pact_with_hierarchy("Foo", "2", "Bar") + .create_deployed_version_for_consumer_version(currently_deployed: true) + .create_pact_with_hierarchy("Waffle", "3", "Bar") + .create_pact_with_hierarchy("Waffle", "4", "Bar") + .create_deployed_version_for_consumer_version(currently_deployed: true) + .create_environment("prod") + .create_pact_with_hierarchy("Foo", "5", "Bar") + .comment("not included, wrong environment") + .create_deployed_version_for_consumer_version(currently_deployed: true) + end + + let(:consumer_version_selectors) do + PactBroker::Pacts::Selectors.new( + PactBroker::Pacts::Selector.for_currently_deployed("test") + ) + end + + it "returns the pacts for the currently deployed versions" do + expect(subject.size).to eq 2 + expect(subject.first.selectors).to eq [PactBroker::Pacts::Selector.for_currently_deployed("test").resolve(td.find_version("Foo", "2"))] + expect(subject.last.selectors).to eq [PactBroker::Pacts::Selector.for_currently_deployed("test").resolve(td.find_version("Waffle", "4"))] + end + end + + context "when currently_deployed is true and an environment is and consumer specified" do + before do + td.create_environment("test") + .create_pact_with_hierarchy("Foo", "1", "Bar") + .create_deployed_version_for_consumer_version(currently_deployed: false) + .create_pact_with_hierarchy("Foo", "2", "Bar") + .create_deployed_version_for_consumer_version(currently_deployed: true) + .create_pact_with_hierarchy("Waffle", "3", "Bar") + .create_pact_with_hierarchy("Waffle", "4", "Bar") + .create_deployed_version_for_consumer_version(currently_deployed: true) + .create_environment("prod") + .create_pact_with_hierarchy("Foo", "5", "Bar") + .comment("not included, wrong environment") + .create_deployed_version_for_consumer_version(currently_deployed: true) + end + + let(:consumer_version_selectors) do + PactBroker::Pacts::Selectors.new( + PactBroker::Pacts::Selector.for_currently_deployed_and_environment_and_consumer("test", "Foo") + ) + end + + it "returns the pacts for the currently deployed versions" do + expect(subject.size).to eq 1 + expect(subject.first.selectors).to eq [PactBroker::Pacts::Selector.for_currently_deployed_and_environment_and_consumer("test", "Foo").resolve(td.find_version("Foo", "2"))] + end + end + + context "when the same version is deployed to multiple environments" do + before do + td.create_environment("test") + .create_environment("prod") + .create_pact_with_hierarchy("Foo", "1", "Bar") + .create_deployed_version_for_consumer_version(environment_name: "test") + .create_deployed_version_for_consumer_version(environment_name: "prod") + end + + let(:consumer_version_selectors) do + PactBroker::Pacts::Selectors.new( + PactBroker::Pacts::Selector.for_currently_deployed + ) + end + + it "returns one pact_publication with multiple selectors" do + expect(subject.size).to eq 1 + expect(subject.first.selectors.size).to eq 2 + expect(subject.first.selectors.first.environment).to eq "test" + expect(subject.first.selectors.last.environment).to eq "prod" + end + end + end + end + end +end diff --git a/spec/lib/pact_broker/pacts/selector_spec.rb b/spec/lib/pact_broker/pacts/selector_spec.rb index 48781960b..5e28d1894 100644 --- a/spec/lib/pact_broker/pacts/selector_spec.rb +++ b/spec/lib/pact_broker/pacts/selector_spec.rb @@ -14,13 +14,15 @@ module Pacts let(:all_dev_for_consumer_1) { Selector.all_for_tag_and_consumer('dev', 'Bar') } let(:all_prod) { Selector.all_for_tag('prod') } let(:all_dev) { Selector.all_for_tag('dev') } + let(:currently_deployed_to_prod) { Selector.for_currently_deployed('prod') } + let(:currently_deployed_to_test) { Selector.for_currently_deployed('test') } let(:unsorted_selectors) do - [all_prod, all_dev, all_dev_for_consumer_1, latest_for_branch_main, latest_for_tag_prod, overall_latest_1, overall_latest_1, latest_for_tag_dev, all_prod_for_consumer_2, all_prod_for_consumer_1] + [all_prod, all_dev, currently_deployed_to_prod, all_dev_for_consumer_1, latest_for_branch_main, latest_for_tag_prod, currently_deployed_to_test, overall_latest_1, overall_latest_1, latest_for_tag_dev, all_prod_for_consumer_2, all_prod_for_consumer_1] end let(:expected_sorted_selectors) do - [overall_latest_1, overall_latest_1, latest_for_branch_main, latest_for_tag_dev, latest_for_tag_prod, all_dev_for_consumer_1, all_prod_for_consumer_2, all_prod_for_consumer_1, all_dev, all_prod] + [overall_latest_1, overall_latest_1, latest_for_branch_main, currently_deployed_to_prod, currently_deployed_to_test, latest_for_tag_dev, latest_for_tag_prod, all_dev_for_consumer_1, all_prod_for_consumer_2, all_prod_for_consumer_1, all_dev, all_prod] end it "sorts the selectors" do diff --git a/spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb b/spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb index 218a941e9..cedf992ac 100644 --- a/spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb +++ b/spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb @@ -121,6 +121,33 @@ module Pacts its(:inclusion_reason) { is_expected.to include "The pact at http://pact is being verified because it matches the following configured selection criterion: latest pact between Foo and Bar"} end + + context "when the consumer version is currently deployed to a single environment" do + let(:selectors) { Selectors.new(Selector.for_currently_deployed('test')) } + + its(:inclusion_reason) { is_expected.to include "The pact at http://pact is being verified because it matches the following configured selection criterion: pacts for the consumer version(s) currently deployed to test"} + end + + context "when the consumer version is currently deployed to a multiple environments" do + let(:selectors) { Selectors.new(Selector.for_currently_deployed('dev'), Selector.for_currently_deployed('test'), Selector.for_currently_deployed('prod')) } + + its(:inclusion_reason) { is_expected.to include "pacts for the consumer version(s) currently deployed to dev, prod and test (all have the same content)"} + end + + context "when the currently deployed consumer version is for a consumer" do + let(:selectors) do + Selectors.new( + Selector.for_currently_deployed_and_environment_and_consumer('test', 'Foo'), + Selector.for_currently_deployed_and_environment_and_consumer('prod', 'Foo'), + Selector.for_currently_deployed_and_environment_and_consumer('test', 'Bar'), + Selector.for_currently_deployed('test'), + ) + end + + its(:inclusion_reason) { is_expected.to include "pacts for the version(s) of Foo currently deployed to prod and test"} + its(:inclusion_reason) { is_expected.to include "pacts for the version(s) of Bar currently deployed to test"} + its(:inclusion_reason) { is_expected.to include "pacts for the consumer version(s) currently deployed to test"} + end end describe "#pending_reason" do