Skip to content

Commit

Permalink
feat(matrix): allow provider to be deployed to an environment without…
Browse files Browse the repository at this point in the history
… the consumer having to be deployed there already

Closes: pact-foundation/pact_broker-client#48
  • Loading branch information
bethesque committed Mar 31, 2019
1 parent 2e2a203 commit 125c272
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 84 deletions.
31 changes: 15 additions & 16 deletions lib/pact_broker/matrix/deployment_status_summary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,24 @@ def counts
{
success: rows.count{ |row| row.success },
failed: rows.count { |row| row.success == false },
unknown: integrations_without_a_row.count + rows.count { |row| row.success.nil? }
unknown: required_integrations_without_a_row.count + rows.count { |row| row.success.nil? }
}
end

def deployable?
return nil if rows.empty?
# return nil if rows.empty?
return nil if rows.any?{ |row| row.success.nil? }
return nil if integrations_without_a_row.any?
return nil if required_integrations_without_a_row.any?
rows.all?{ |row| row.success }
end

def reasons
@reasons ||= begin
reasons = []
if rows.empty?
reasons << "No results matched the given query"
else
reasons.concat(missing_reasons)
reasons.concat(failure_messages)
reasons.concat(unverified_messages)
reasons.concat(success_messages)
end
reasons.concat(missing_reasons)
reasons.concat(failure_messages)
reasons.concat(unverified_messages)
reasons.concat(success_messages)
reasons
end
end
Expand All @@ -60,16 +56,19 @@ def failure_messages
end

def success_messages
if rows.all?{ |row| row.success } && integrations_without_a_row.empty?
if rows.all?{ |row| row.success } && required_integrations_without_a_row.empty?
["All verification results are published and successful"]
else
[]
end
end

def integrations_without_a_row
@integrations_without_a_row ||= begin
integrations.select do | relationship |
# For deployment, the consumer requires the provider,
# but the provider does not require the consumer
# This method tells us which providers are missing.
def required_integrations_without_a_row
@required_integrations_without_a_row ||= begin
integrations.select(&:required?).select do | relationship |
!rows.find do | row |
row.consumer_id == relationship.consumer_id && row.provider_id == relationship.provider_id
end
Expand All @@ -78,7 +77,7 @@ def integrations_without_a_row
end

def missing_reasons
integrations_without_a_row.collect do | missing_relationship|
required_integrations_without_a_row.collect do | missing_relationship|
consumer_version_desc = "#{missing_relationship.consumer_name} (#{resolved_version_for(missing_relationship.consumer_id)})"
provider_version_desc = "#{missing_relationship.provider_name} (#{resolved_version_for(missing_relationship.provider_id)})"
"There is no verified pact between #{consumer_version_desc} and #{provider_version_desc}"
Expand Down
38 changes: 32 additions & 6 deletions lib/pact_broker/matrix/integration.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,46 @@
#
# Represents the integration relationship between a consumer and a provider
#

# Represents the integration relationship between a consumer and a provider in the context
# of a matrix or can-i-deploy query.
# If the required flag is set, then one of the pacticipants (consumers) specified in the HTTP query
# requires the provider. It would not be required if a provider was specified, and it had an
# integration with a consumer.

module PactBroker
module Matrix
class Integration

attr_reader :consumer_name, :consumer_id, :provider_name, :provider_id

def initialize consumer_id, consumer_name, provider_id, provider_name
def initialize consumer_id, consumer_name, provider_id, provider_name, required
@consumer_id = consumer_id
@consumer_name = consumer_name
@provider_id = provider_id
@provider_name = provider_name
@required = required
end

def self.from_hash hash
new(
hash.fetch(:consumer_id),
hash.fetch(:consumer_name),
hash.fetch(:provider_id),
hash.fetch(:provider_name)
hash.fetch(:provider_name),
hash.fetch(:required)
)
end

def required?
@required
end

def == other
consumer_id == other.consumer_id && provider_id == other.provider_id
end

def <=> other
comparison = consumer_name <=> other.consumer_name
return comparison if comparison != 0
provider_name <=> other.provider_name
comparison =provider_name <=> other.provider_name
end

def to_hash
Expand All @@ -49,6 +59,22 @@ def pacticipant_names
def to_s
"Relationship between #{consumer_name} (id=#{consumer_id}) and #{provider_name} (id=#{provider_id})"
end

def involves_consumer_with_id?(consumer_id)
self.consumer_id == consumer_id
end

def involves_consumer_with_names?(consumer_names)
consumer_names.include?(self.consumer_name)
end

def involves_provider_with_name?(provider_name)
self.provider_name == provider_name
end

def involves_consumer_with_name?(consumer_name)
self.consumer_name == consumer_name
end
end
end
end
134 changes: 85 additions & 49 deletions lib/pact_broker/matrix/repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'pact_broker/matrix/query_results'
require 'pact_broker/matrix/integration'
require 'pact_broker/matrix/query_results_with_deployment_status_summary'
require 'pact_broker/matrix/resolved_selector'

module PactBroker
module Matrix
Expand Down Expand Up @@ -48,18 +49,18 @@ def find_ids_for_pacticipant_names params
end

# Return the latest matrix row (pact/verification) for each consumer_version_number/provider_version_number
def find selectors, options = {}
resolved_selectors = resolve_selectors(selectors, options)
def find specified_selectors, options = {}
resolved_selectors = resolve_selectors(specified_selectors, options)
lines = query_matrix(resolved_selectors, options)
lines = apply_latestby(options, selectors, lines)
lines = apply_latestby(options, specified_selectors, lines)

# This needs to be done after the latestby, so can't be done in the db unless
# the latestby logic is moved to the db
if options.key?(:success)
lines = lines.select{ |l| options[:success].include?(l.success) }
end

QueryResults.new(lines.sort, selectors, options, resolved_selectors)
QueryResults.new(lines.sort, specified_selectors, options, resolved_selectors)
end

def find_for_consumer_and_provider pacticipant_1_name, pacticipant_2_name
Expand All @@ -73,13 +74,19 @@ def find_compatible_pacticipant_versions selectors
end

def find_integrations(pacticipant_names)
selectors = pacticipant_names.collect{ | pacticipant_name | add_ids(pacticipant_name: pacticipant_name) }
selectors = pacticipant_names.collect{ | pacticipant_name | add_ids_to_selector(pacticipant_name: pacticipant_name) }
Row
.select(:consumer_name, :consumer_id, :provider_name, :provider_id)
.matching_selectors(selectors)
.distinct
.all
.collect{ |row | Integration.from_hash(row.to_hash) }.uniq
.collect do |row |
row.to_hash
end
.uniq
.collect do | hash |
Integration.from_hash(hash.merge(required: pacticipant_names.include?(hash[:consumer_name])))
end
end

private
Expand Down Expand Up @@ -127,85 +134,102 @@ def view_for(options)
Row
end

def resolve_selectors(selectors, options)
resolved_selectors = look_up_version_numbers(selectors, options)
def resolve_selectors(specified_selectors, options)
resolved_specified_selectors = resolve_versions_and_add_ids(specified_selectors, options)
if options[:latest] || options[:tag]
apply_latest_and_tag_to_inferred_selectors(resolved_selectors, options)
add_inferred_selectors(resolved_specified_selectors, options)
else
resolved_selectors
resolved_specified_selectors
end
end

# Find the version number for selectors with the latest and/or tag specified
def look_up_version_numbers(selectors, options)
def resolve_versions_and_add_ids(selectors, options, required = true)
selectors.collect do | selector |
if selector[:tag] && selector[:latest]
version = version_repository.find_by_pacticipant_name_and_latest_tag(selector[:pacticipant_name], selector[:tag])
raise Error.new("No version of #{selector[:pacticipant_name]} found with tag #{selector[:tag]}") unless version
# validation in resource should ensure we always have a version
{
pacticipant_name: selector[:pacticipant_name],
pacticipant_version_number: version.number
}
elsif selector[:latest]
version = version_repository.find_latest_by_pacticpant_name(selector[:pacticipant_name])
raise Error.new("No version of #{selector[:pacticipant_name]} found") unless version
{
pacticipant_name: selector[:pacticipant_name],
pacticipant_version_number: version.number
}
elsif selector[:tag]
# validation in resource should ensure we always have at least one version
versions = version_repository.find_by_pacticipant_name_and_tag(selector[:pacticipant_name], selector[:tag])
raise Error.new("No version of #{selector[:pacticipant_name]} found with tag #{selector[:tag]}") unless versions.any?
pacticipant = PactBroker::Domain::Pacticipant.find(name: selector[:pacticipant_name])

versions = find_versions_for_selector(selector, required)

if versions
versions.collect do | version |
{
pacticipant_name: selector[:pacticipant_name],
pacticipant_version_number: version.number
}
if version
selector_for_version(pacticipant, version)
else
selector_for_non_existing_version(pacticipant)
end
end
else
selector.dup
selector_without_version(pacticipant)
end
end.flatten.compact.collect do | selector |
add_ids(selector)
end.flatten
end

def find_versions_for_selector(selector, required)
if selector[:tag] && selector[:latest]
version = version_repository.find_by_pacticipant_name_and_latest_tag(selector[:pacticipant_name], selector[:tag])
# raise Error.new("No version of #{selector[:pacticipant_name]} found with tag #{selector[:tag]}") if required && !version
[version]
elsif selector[:latest]
version = version_repository.find_latest_by_pacticpant_name(selector[:pacticipant_name])
# raise Error.new("No version of #{selector[:pacticipant_name]} found") if required && !version
[version]
elsif selector[:tag]
versions = version_repository.find_by_pacticipant_name_and_tag(selector[:pacticipant_name], selector[:tag])
# raise Error.new("No version of #{selector[:pacticipant_name]} found with tag #{selector[:tag]}") if required && versions.empty?
versions.any? ? versions : [nil]
elsif selector[:pacticipant_version_number]
version = version_repository.find_by_pacticipant_name_and_number(selector[:pacticipant_name], selector[:pacticipant_version_number])
# raise Error.new("No version #{selector[:pacticipant_version_number]} of #{selector[:pacticipant_name]} found") if required && !version
[version]
else
nil
end
end

def add_ids(selector)
def add_ids_to_selector(selector)
if selector[:pacticipant_name]
pacticipant = PactBroker::Domain::Pacticipant.find(name: selector[:pacticipant_name])
selector[:pacticipant_id] = pacticipant ? pacticipant.id : nil
end

if selector[:pacticipant_name] && selector[:pacticipant_version_number]
if selector[:pacticipant_name] && selector[:pacticipant_version_number] && !selector[:pacticipant_version_id]
version = version_repository.find_by_pacticipant_name_and_number(selector[:pacticipant_name], selector[:pacticipant_version_number])
selector[:pacticipant_version_id] = version ? version.id : nil
end

if selector[:pacticipant_version_number].nil?
selector[:pacticipant_version_id] = nil
if !selector.key?(:pacticipant_version_id)
selector[:pacticipant_version_id] = nil
end
selector
end

# eg. when checking to see if Foo version 2 can be deployed to prod,
# need to look up all the 'partner' pacticipants, and determine their latest prod versions
def apply_latest_and_tag_to_inferred_selectors(selectors, options)
all_pacticipant_names = all_pacticipant_names_in_specified_matrix(selectors)
specified_names = selectors.collect{ |s| s[:pacticipant_name] }
inferred_names = all_pacticipant_names - specified_names
def add_inferred_selectors(resolved_specified_selectors, options)
integrations = find_integrations(resolved_specified_selectors.collect{|s| s[:pacticipant_name]})
all_pacticipant_names = integrations.collect(&:pacticipant_names).flatten.uniq
specified_names = resolved_specified_selectors.collect{ |s| s[:pacticipant_name] }
inferred_pacticipant_names = all_pacticipant_names - specified_names
# Inferred providers are required for a consumer to be deployed
required_inferred_pacticipant_names = inferred_pacticipant_names.select{ | n | integrations.any?{ |i| i.involves_provider_with_name?(n) } }
# Inferred consumers are NOT required for a provider to be deployed
optional_inferred_pacticipant_names = inferred_pacticipant_names - required_inferred_pacticipant_names

resolved_specified_selectors +
build_inferred_selectors(required_inferred_pacticipant_names, options, true) +
build_inferred_selectors(optional_inferred_pacticipant_names, options, false)
end

inferred_selectors = inferred_names.collect do | pacticipant_name |
def build_inferred_selectors(inferred_pacticipant_names, options, required)
selectors = inferred_pacticipant_names.collect do | pacticipant_name |
selector = {
pacticipant_name: pacticipant_name,
pacticipant_name: pacticipant_name
}
selector[:tag] = options[:tag] if options[:tag]
selector[:latest] = options[:latest] if options[:latest]
selector
end

selectors + look_up_version_numbers(inferred_selectors, options)
resolve_versions_and_add_ids(selectors, options, required)
end

def all_pacticipant_names_in_specified_matrix(selectors)
Expand All @@ -214,6 +238,18 @@ def all_pacticipant_names_in_specified_matrix(selectors)
.flatten
.uniq
end

def selector_for_non_existing_version(pacticipant)
ResolvedSelector.for_pacticipant_and_non_existing_version(pacticipant)
end

def selector_for_version(pacticipant, version)
ResolvedSelector.for_pacticipant_and_version(pacticipant, version)
end

def selector_without_version(pacticipant)
ResolvedSelector.for_pacticipant(pacticipant)
end
end
end
end
Loading

0 comments on commit 125c272

Please sign in to comment.