Skip to content

Commit

Permalink
feat: ignore pacticipant for can i deploy (#429)
Browse files Browse the repository at this point in the history
  • Loading branch information
bethesque authored May 17, 2021
1 parent 57f10f0 commit 25e62fd
Show file tree
Hide file tree
Showing 23 changed files with 820 additions and 302 deletions.
27 changes: 20 additions & 7 deletions lib/pact_broker/api/decorators/matrix_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ def to_hash(options)
deployable: deployable,
reason: reason
},
notices: notices,
matrix: matrix(options[:user_options][:base_url])
}.tap do | hash |
hash[:summary].merge!(query_results_with_deployment_status_summary.deployment_status_summary.counts)
end

end

def deployable
Expand All @@ -52,15 +52,21 @@ def reason_decorator_class
end

def matrix(base_url)
query_results_with_deployment_status_summary.rows.collect do | line |
provider = OpenStruct.new(name: line.provider_name)
consumer = OpenStruct.new(name: line.consumer_name)
consumer_version = OpenStruct.new(number: line.consumer_version_number, pacticipant: consumer)
provider_version = line.provider_version_number ? OpenStruct.new(number: line.provider_version_number, pacticipant: provider) : nil
line_hash(consumer, provider, consumer_version, provider_version, line, base_url)
query_results_with_deployment_status_summary.considered_rows.collect do | line |
hash_for_row(line, base_url)
end + query_results_with_deployment_status_summary.ignored_rows.collect do | line |
hash_for_row(line, base_url).merge(ignored: true)
end
end

def hash_for_row(line, base_url)
provider = OpenStruct.new(name: line.provider_name)
consumer = OpenStruct.new(name: line.consumer_name)
consumer_version = OpenStruct.new(number: line.consumer_version_number, pacticipant: consumer)
provider_version = line.provider_version_number ? OpenStruct.new(number: line.provider_version_number, pacticipant: provider) : nil
line_hash(consumer, provider, consumer_version, provider_version, line, base_url)
end

def line_hash(consumer, provider, consumer_version, provider_version, line, base_url)
{
consumer: consumer_hash(line, consumer, consumer_version, base_url),
Expand Down Expand Up @@ -163,6 +169,13 @@ def verification_hash(line, base_url)
nil
end
end

def notices
query_results_with_deployment_status_summary
.deployment_status_summary
.reasons
.collect{ | reason | { type: reason.type, text: reason_decorator_class.new(reason).to_s } }
end
end
end
end
Expand Down
26 changes: 18 additions & 8 deletions lib/pact_broker/api/decorators/reason_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,29 @@ module Api
module Decorators
class ReasonDecorator
def initialize(reason)
@reason = reason
if reason.is_a?(PactBroker::Matrix::IgnoredReason)
@reason = reason.root_reason
@ignored = true
else
@reason = reason
@ignored = false
end
end

def to_s
(ignored ? "Ignoring: " : "") + reason_text
end

private

attr_reader :reason, :ignored

def reason_text
case reason
when PactBroker::Matrix::PactNotEverVerifiedByProvider
"There is no verified pact between #{reason.consumer_selector.description} and #{reason.provider_selector.description}"
when PactBroker::Matrix::PactNotVerifiedByRequiredProviderVersion
"There is no verified pact between #{reason.consumer_selector.description} and #{reason.provider_selector.description}"
# when PactBroker::Matrix::VerificationFailed
# "The pact verification between #{reason.consumer_selector.description} and #{reason.provider_selector.description} failed"
when PactBroker::Matrix::SpecifiedVersionDoesNotExist
version_does_not_exist_description(reason.selector)
when PactBroker::Matrix::VerificationFailed
Expand All @@ -28,16 +40,14 @@ def to_s
descriptions = reason.interactions.collect do | interaction |
interaction_description(interaction)
end.join('; ')
"WARNING: Although the verification was reported as successful, the results for #{reason.consumer_selector.description} and #{reason.provider_selector.description} may be missing tests for the following interactions: #{descriptions}"
"WARN: Although the verification was reported as successful, the results for #{reason.consumer_selector.description} and #{reason.provider_selector.description} may be missing tests for the following interactions: #{descriptions}"
when PactBroker::Matrix::IgnoreSelectorDoesNotExist
"WARN: Cannot ignore #{reason.selector.description}"
else
reason
end
end

private

attr_reader :reason

def version_does_not_exist_description selector
if selector.version_does_not_exist?
if selector.tag
Expand Down
6 changes: 4 additions & 2 deletions lib/pact_broker/doc/views/can-i-deploy.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ A simplified resource that accepts the same parameters as the basic usage of the

* _pacticipant_: The name of the pacticipant (application) you want to deploy (required).
* _version_: The version of the pacticipant (application) you want to deploy (required).
* _to_: The tag used to identify the environment into which you wish to deploy the application (eg. `test` or `prod`). This assumes you have already tagged the currently deployed versions of each of the integrated applications with the same tag. To be specific, the logic checks if the application version you have specified is compatible with the latest versions _for the specified tag_ of all the other applications it is integrated with. This parameter is optional - if not specified, it checks for compatiblity with the latest version of all the integrated applications.
* _environment_: The name of the environment into which the pacticipant (application) is to be deployed.
* _to_: The tag used to identify the environment into which you wish to deploy the application (eg. `test` or `prod`). Deprecated - use the `environment=ENVIRONMENT` parameter in preference to the `to=TAG` parameter as deployments and environments are now explictly supported.
* _ignore[]_: The name of the pacticipant to ignore when determining if it is safe to deploy (optional). May be used multiple times.


If you have an environment that you identify with the tag `prod`, and each time you deployed an application to the prod environment you tagged the relevant application version in the Pact Broker with the tag `prod`, then calling `/can-i-deploy?pacticipant=Foo&version=734137278d&to=prod` will check that version 734137278d of Foo has a successful verification result with each of the integrated application versions that are currently in prod. That is, it is safe to deploy.
If you have an environment that you identify with the name `prod`, and each time you deployed an application to the prod environment you recorded the deployment of relevant application version in the Pact Broker using `record-deployment`, then calling `/can-i-deploy?pacticipant=Foo&version=734137278d&environment=prod` will check that version 734137278d of Foo has a successful verification result with each of the integrated application versions that are currently in prod. That is, it is safe to deploy.
78 changes: 42 additions & 36 deletions lib/pact_broker/matrix/deployment_status_summary.rb
Original file line number Diff line number Diff line change
@@ -1,37 +1,40 @@
require 'pact_broker/logging'
require 'pact_broker/matrix/reason'
require 'forwardable'

module PactBroker
module Matrix
class DeploymentStatusSummary
include PactBroker::Logging
extend Forwardable

attr_reader :rows, :resolved_selectors, :integrations
attr_reader :query_results, :all_rows
delegate [:considered_rows, :ignored_rows, :resolved_selectors, :resolved_ignore_selectors, :integrations] => :query_results

def initialize(rows, resolved_selectors, integrations)
@rows = rows
@resolved_selectors = resolved_selectors
@integrations = integrations
def initialize(query_results)
@query_results = query_results
@all_rows = query_results.rows
@dummy_selectors = create_dummy_selectors
end

def counts
{
success: rows.count(&:success),
failed: rows.count { |row| row.success == false },
unknown: required_integrations_without_a_row.count + rows.count { |row| row.success.nil? }
}
success: considered_rows.count(&:success),
failed: considered_rows.count { |row| row.success == false },
unknown: required_integrations_without_a_row.count + considered_rows.count { |row| row.success.nil? },
ignored: resolved_ignore_selectors.any? ? ignored_rows.count : nil
}.compact
end

def deployable?
return false if specified_selectors_that_do_not_exist.any?
return nil if rows.any?{ |row| row.success.nil? }
return false if considered_specified_selectors_that_do_not_exist.any?
return nil if considered_rows.any?{ |row| row.success.nil? }
return nil if required_integrations_without_a_row.any?
rows.all?(&:success) # true if rows is empty
considered_rows.all?(&:success) # true if considered_rows is empty
end

def reasons
error_messages.any? ? error_messages + warning_messages : success_messages + warning_messages
error_messages.any? ? warning_messages + error_messages : warning_messages + success_messages
end

private
Expand All @@ -52,32 +55,35 @@ def error_messages
end

def warning_messages
[]
resolved_ignore_selectors.select(&:pacticipant_or_version_does_not_exist?).collect { | s | IgnoreSelectorDoesNotExist.new(s) }
# ignored_rows.select{ | row | row.success.nil? }.collect{ |row | IgnoredReason.new(pact_not_ever_verified_by_provider(row)) } +
# specified_selectors_that_do_not_exist.select(&:ignore?).collect { | selector | IgnoredReason.new(SpecifiedVersionDoesNotExist.new(selector)) } +
# ignored_rows.select{ |row| row.success == false }.collect { | row | IgnoredReason.new(VerificationFailed.new(*selectors_for(row))) }
end

def considered_specified_selectors_that_do_not_exist
resolved_selectors.select(&:consider?).select(&:specified_version_that_does_not_exist?)
end

def specified_selectors_that_do_not_exist
resolved_selectors.select(&:specified_version_that_does_not_exist?)
end

def specified_selectors_do_not_exist_messages
specified_selectors_that_do_not_exist.collect do | selector |
SpecifiedVersionDoesNotExist.new(selector)
end
specified_selectors_that_do_not_exist.select(&:consider?).collect { | selector | SpecifiedVersionDoesNotExist.new(selector) }
end

def not_ever_verified_reasons
rows.select{ | row | row.success.nil? }.collect{ |row | pact_not_ever_verified_by_provider(row) }
considered_rows.select{ | row | row.success.nil? }.collect{ |row | pact_not_ever_verified_by_provider(row) }
end

def failure_messages
rows.select{ |row| row.success == false }.collect do | row |
VerificationFailed.new(*selectors_for(row))
end
considered_rows.select{ |row| row.success == false }.collect { | row | VerificationFailed.new(*selectors_for(row)) }
end

def success_messages
if rows.all?(&:success) && required_integrations_without_a_row.empty?
if rows.any?
if considered_rows.all?(&:success) && required_integrations_without_a_row.empty?
if considered_rows.any?
[Successful.new]
else
[NoDependenciesMissing.new]
Expand All @@ -99,7 +105,7 @@ def success_messages
# Foo v2 -> Bar v1 (latest prod) [this line not included because CV doesn't match]
# Foo v3 -> Bar v2 [this line not included because PV doesn't match]
#
# No matrix rows would be returned. This method identifies that we have no row for
# No matrix considered_rows would be returned. This method identifies that we have no row for
# the Foo -> Bar integration, and therefore cannot deploy Foo.
# However, if we were to try and deploy the provider, Bar, that would be ok
# as Bar does not rely on Foo, so this method would not return that integration.
Expand All @@ -117,11 +123,11 @@ def required_integrations_without_a_row
end

def log_required_integrations_without_a_row_occurred integrations
logger.info("required_integrations_without_a_row returned non empty", payload: { integrations: integrations, rows: rows })
logger.info("required_integrations_without_a_row returned non empty", payload: { integrations: integrations, considered_rows: considered_rows })
end

def row_exists_for_integration(integration)
rows.find { | row | integration.matches_pacticipant_ids?(row) }
all_rows.find { | row | integration.matches_pacticipant_ids?(row) }
end

def missing_reasons
Expand Down Expand Up @@ -163,33 +169,33 @@ def selectors_for(row)

# When the user has not specified a version of the provider (eg no 'latest' and/or 'tag', which means 'all versions')
# so the "add inferred selectors" code in the Matrix::Repository has not run,
# we may end up with rows for which we do not have a selector.
# we may end up with considered_rows for which we do not have a selector.
# To solve this, create dummy selectors from the row and integration data.
def create_dummy_selectors
(dummy_selectors_from_rows + dummy_selectors_from_integrations).uniq
(dummy_selectors_from_considered_rows + dummy_selectors_from_integrations).uniq
end

def dummy_selectors_from_integrations
integrations.collect do | row |
dummy_consumer_selector = ResolvedSelector.for_pacticipant(row.consumer, :inferred)
dummy_provider_selector = ResolvedSelector.for_pacticipant(row.provider, :inferred)
dummy_consumer_selector = ResolvedSelector.for_pacticipant(row.consumer, :inferred, false)
dummy_provider_selector = ResolvedSelector.for_pacticipant(row.provider, :inferred, false)
[dummy_consumer_selector, dummy_provider_selector]
end.flatten
end

def dummy_selectors_from_rows
rows.collect do | row |
dummy_consumer_selector = ResolvedSelector.for_pacticipant_and_version(row.consumer, row.consumer_version, {}, :inferred)
def dummy_selectors_from_considered_rows
considered_rows.collect do | row |
dummy_consumer_selector = ResolvedSelector.for_pacticipant_and_version(row.consumer, row.consumer_version, {}, :inferred, false)
dummy_provider_selector = row.provider_version ?
ResolvedSelector.for_pacticipant_and_version(row.provider, row.provider_version, {}, :inferred) :
ResolvedSelector.for_pacticipant(row.provider, :inferred)
ResolvedSelector.for_pacticipant_and_version(row.provider, row.provider_version, {}, :inferred, false) :
ResolvedSelector.for_pacticipant(row.provider, :inferred, false)
[dummy_consumer_selector, dummy_provider_selector]
end.flatten
end

# experimental
def warnings_for_missing_interactions
rows.select(&:success).collect do | row |
considered_rows.select(&:success).collect do | row |
begin
if row.verification.interactions_missing_test_results.any? && !row.verification.all_interactions_missing_test_results?
InteractionsMissingVerifications.new(selector_for(row.consumer_name), selector_for(row.provider_name), row.verification.interactions_missing_test_results)
Expand Down
10 changes: 10 additions & 0 deletions lib/pact_broker/matrix/parse_can_i_deploy_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ def self.call params
options[:environment_name] = params[:environment]
end

if params[:ignore].is_a?(Array)
options[:ignore_selectors] = params[:ignore].collect do | pacticipant_name |
if pacticipant_name.is_a?(String)
PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: pacticipant_name)
end
end.compact
else
options[:ignore_selectors] = []
end

return [selector], options
end
end
Expand Down
24 changes: 17 additions & 7 deletions lib/pact_broker/matrix/parse_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@ class ParseQuery
def self.call query
params = Rack::Utils.parse_nested_query(query)
selectors = (params['q'] || []).collect do |i|
p = PactBroker::Matrix::UnresolvedSelector.new
p.pacticipant_name = i['pacticipant'] if i['pacticipant'] && i['pacticipant'] != ''
p.pacticipant_version_number = i['version'] if i['version'] && i['version'] != ''
p.latest = true if i['latest'] == 'true'
p.branch = i['branch'] if i['branch'] && i['branch'] != ''
p.tag = i['tag'] if i['tag'] && i['tag'] != ''
p
parse_selector(i)
end
options = {}
if params.key?('success') && params['success'].is_a?(Array)
Expand Down Expand Up @@ -52,8 +46,24 @@ def self.call query
options[:environment_name] = params['environment']
end

if params['ignore'].is_a?(Array)
options[:ignore_selectors] = params['ignore'].collect{ |i| parse_selector(i) }
else
options[:ignore_selectors] = []
end

return selectors, options
end

def self.parse_selector(i)
p = PactBroker::Matrix::UnresolvedSelector.new
p.pacticipant_name = i['pacticipant'] if i['pacticipant'] && i['pacticipant'] != ''
p.pacticipant_version_number = i['version'] if i['version'] && i['version'] != ''
p.latest = true if i['latest'] == 'true'
p.branch = i['branch'] if i['branch'] && i['branch'] != ''
p.tag = i['tag'] if i['tag'] && i['tag'] != ''
p
end
end
end
end
11 changes: 7 additions & 4 deletions lib/pact_broker/matrix/query_results.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
module PactBroker
module Matrix
class QueryResults < Array
attr_reader :selectors, :options, :resolved_selectors, :integrations
attr_reader :considered_rows, :ignored_rows, :selectors, :options, :resolved_selectors, :resolved_ignore_selectors, :integrations

def initialize rows, selectors, options, resolved_selectors, integrations
super(rows)
def initialize considered_rows, ignored_rows, selectors, options, resolved_selectors, resolved_ignore_selectors, integrations
super(considered_rows + ignored_rows)
@considered_rows = considered_rows
@ignored_rows = ignored_rows
@selectors = selectors
@resolved_selectors = resolved_selectors
@options = options
@resolved_selectors = resolved_selectors
@resolved_ignore_selectors = resolved_ignore_selectors
@integrations = integrations
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class QueryResultsWithDeploymentStatusSummary

attr_reader :query_results, :deployment_status_summary

delegate [:selectors, :options, :resolved_selectors, :integrations] => :query_results
delegate [:selectors, :options, :resolved_selectors, :integrations, :considered_rows, :ignored_rows] => :query_results
delegate (Array.instance_methods - Object.instance_methods) => :rows
delegate [:deployable?] => :deployment_status_summary

Expand Down
Loading

0 comments on commit 25e62fd

Please sign in to comment.