Skip to content

Commit

Permalink
feat(webhooks): support ${pactbroker.currentlyDeployedProviderVersion…
Browse files Browse the repository at this point in the history
…Number} in webhook templates (#402)
  • Loading branch information
bethesque authored Mar 22, 2021
1 parent 0429398 commit 1e5487f
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 19 deletions.
4 changes: 4 additions & 0 deletions lib/pact_broker/deployments/deployed_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ def order_by_date_desc
def record_undeployed
update(currently_deployed: false, undeployed_at: Sequel.datetime_class.now)
end

def version_number
version.number
end
end
end
end
9 changes: 9 additions & 0 deletions lib/pact_broker/deployments/deployed_version_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ def self.find_deployed_versions_for_environment(environment)
.all
end

def self.find_currently_deployed_versions_for_pacticipant(pacticipant)
DeployedVersion
.currently_deployed
.where(pacticipant_id: pacticipant.id)
.eager(:version)
.eager(:environment)
.all
end

def self.record_previous_version_undeployed(pacticipant, environment)
DeployedVersion.last_deployed_version(pacticipant, environment)&.record_undeployed
end
Expand Down
5 changes: 5 additions & 0 deletions lib/pact_broker/domain/webhook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'pact_broker/logging'
require 'pact_broker/api/contracts/webhook_contract'
require 'pact_broker/webhooks/http_request_with_redacted_headers'
require 'pact_broker/webhooks/pact_and_verification_parameters'

module PactBroker
module Domain
Expand Down Expand Up @@ -96,6 +97,10 @@ def trigger_on_provider_verification_failed?
events.any?(&:provider_verification_failed?)
end

def expand_currently_deployed_provider_versions?
request.uses_parameter?(PactBroker::Webhooks::PactAndVerificationParameters::CURRENTLY_DEPLOYED_PROVIDER_VERSION_NUMBER)
end

private

def execute_request(webhook_request)
Expand Down
21 changes: 20 additions & 1 deletion lib/pact_broker/test/http_test_data_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ def deploy_to_prod(pacticipant:, version:)
self
end

def record_deployment(pacticipant:, version:, environment_name:)
puts "Recoding deployment of #{pacticipant} version #{version} to #{environment_name}"
version_body = client.get("/pacticipants/#{encode(pacticipant)}/versions/#{encode(version)}").tap { |response| check_for_error(response) }.body
environment_relation = version_body["_links"]["pb:record-deployment"].find { |relation| relation["name"] == environment_name }
client.post(environment_relation["href"], { replacedPreviousDeployedVersion: true }).tap { |response| check_for_error(response) }
separate
self
end

def create_environment(name:, production: false)
puts "Creating environment #{name}"
client.post("/environments", { name: name, displayName: name, production: production }).tap { |response| check_for_error(response) }
separate
self
end

def create_pacticipant(name)
puts "Creating pacticipant with name #{name}"
client.post("pacticipants", { name: name }).tap { |response| check_for_error(response) }
Expand Down Expand Up @@ -157,7 +173,10 @@ def create_global_webhook_for_contract_changed(uuid: nil, url: "https://postman-
}],
"request" => {
"method" => "POST",
"url" => url
"url" => url,
"body" => {
"deployedProviderVersion" => "${pactbroker.currentlyDeployedProviderVersionNumber}"
}
}
}
path = "webhooks/#{uuid}"
Expand Down
11 changes: 9 additions & 2 deletions lib/pact_broker/webhooks/pact_and_verification_parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class PactAndVerificationParameters
CONSUMER_LABELS = 'pactbroker.consumerLabels'
PROVIDER_LABELS = 'pactbroker.providerLabels'
EVENT_NAME = 'pactbroker.eventName'
CURRENTLY_DEPLOYED_PROVIDER_VERSION_NUMBER = 'pactbroker.currentlyDeployedProviderVersionNumber'

ALL = [
CONSUMER_NAME,
Expand All @@ -28,7 +29,8 @@ class PactAndVerificationParameters
BITBUCKET_VERIFICATION_STATUS,
CONSUMER_LABELS,
PROVIDER_LABELS,
EVENT_NAME
EVENT_NAME,
CURRENTLY_DEPLOYED_PROVIDER_VERSION_NUMBER
]

def initialize(pact, trigger_verification, webhook_context)
Expand All @@ -52,7 +54,8 @@ def to_hash
BITBUCKET_VERIFICATION_STATUS => bitbucket_verification_status,
CONSUMER_LABELS => pacticipant_labels(pact && pact.consumer),
PROVIDER_LABELS => pacticipant_labels(pact && pact.provider),
EVENT_NAME => event_name
EVENT_NAME => event_name,
CURRENTLY_DEPLOYED_PROVIDER_VERSION_NUMBER => currently_deployed_provider_version_number
}
end

Expand Down Expand Up @@ -123,6 +126,10 @@ def pacticipant_labels pacticipant
def event_name
webhook_context.fetch(:event_name)
end

def currently_deployed_provider_version_number
webhook_context[:currently_deployed_provider_version_number] || ""
end
end
end
end
36 changes: 24 additions & 12 deletions lib/pact_broker/webhooks/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
require 'pact_broker/webhooks/execution_configuration'
require 'pact_broker/messages'
require 'pact_broker/webhooks/pact_and_verification_parameters'
require 'pact_broker/feature_toggle'

module PactBroker
module Webhooks
Expand Down Expand Up @@ -128,28 +129,39 @@ def self.trigger_webhooks pact, verification, event_name, event_context, options
webhook_execution_configuration = options.fetch(:webhook_execution_configuration).with_webhook_context(event_name: event_name)
# bit messy to merge in base_url here, but easier than a big refactor
base_url = options.fetch(:webhook_execution_configuration).webhook_context.fetch(:base_url)
run_later(webhooks, pact, verification, event_name, event_context.merge(event_name: event_name, base_url: base_url), options.merge(webhook_execution_configuration: webhook_execution_configuration))

run_webhooks_later(webhooks, pact, verification, event_name, event_context.merge(event_name: event_name, base_url: base_url), options.merge(webhook_execution_configuration: webhook_execution_configuration))
else
logger.info "No enabled webhooks found for consumer \"#{pact.consumer.name}\" and provider \"#{pact.provider.name}\" and event #{event_name}"
end
end

def self.run_later webhooks, pact, verification, event_name, event_context, options
trigger_uuid = next_uuid
def self.run_webhooks_later webhooks, pact, verification, event_name, event_context, options
webhooks.each do | webhook |
begin
triggered_webhook = webhook_repository.create_triggered_webhook(trigger_uuid, webhook, pact, verification, RESOURCE_CREATION, event_name, event_context)
logger.info "Scheduling job for webhook with uuid #{webhook.uuid}"
logger.debug "Schedule webhook with options #{options}"
job_data = { triggered_webhook: triggered_webhook }.deep_merge(options)
# Delay slightly to make sure the request transaction has finished before we execute the webhook
Job.perform_in(5, job_data)
rescue StandardError => e
logger.warn("Error scheduling webhook execution for webhook with uuid #{webhook.uuid}", e)
if PactBroker.feature_enabled?(:expand_currently_deployed_provider_versions) && webhook.expand_currently_deployed_provider_versions?
deployed_version_service.find_currently_deployed_versions_for_pacticipant(pact.provider).collect(&:version_number).uniq.each_with_index do | version_number, index |
schedule_webhook(webhook, pact, verification, event_name, event_context.merge(currently_deployed_provider_version_number: version_number), options, index * 5)
end
else
schedule_webhook(webhook, pact, verification, event_name, event_context, options)
end
end
end

def self.schedule_webhook(webhook, pact, verification, event_name, event_context, options, extra_delay = 0)
begin
trigger_uuid = next_uuid
triggered_webhook = webhook_repository.create_triggered_webhook(trigger_uuid, webhook, pact, verification, RESOURCE_CREATION, event_name, event_context)
logger.info "Scheduling job for webhook with uuid #{webhook.uuid}, context: #{event_context}"
logger.debug "Schedule webhook with options #{options}"
job_data = { triggered_webhook: triggered_webhook }.deep_merge(options)
# Delay slightly to make sure the request transaction has finished before we execute the webhook
Job.perform_in(5 + extra_delay, job_data)
rescue StandardError => e
logger.warn("Error scheduling webhook execution for webhook with uuid #{webhook.uuid}", e)
end
end

def self.find_latest_triggered_webhooks_for_pact pact
webhook_repository.find_latest_triggered_webhooks_for_pact pact
end
Expand Down
7 changes: 7 additions & 0 deletions lib/pact_broker/webhooks/webhook_request_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ def headers= headers
@headers = Rack::Utils::HeaderHash.new(headers)
end

def uses_parameter?(parameter_name)
!!body_string&.include?("${" + parameter_name + "}")
end

def body_string
String === body ? body : body&.to_json
end

def to_s
"#{method.upcase} #{url}, username=#{username}, password=#{display_password}, headers=#{redacted_headers}, body=#{body}"
Expand Down
47 changes: 47 additions & 0 deletions script/reproduce-issue-expand-currently-deployed.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env ruby
begin

$LOAD_PATH << "#{Dir.pwd}/lib"
require 'pact_broker/test/http_test_data_builder'
base_url = ENV['PACT_BROKER_BASE_URL'] || 'http://localhost:9292'

td = PactBroker::Test::HttpTestDataBuilder.new(base_url)
td.delete_integration(consumer: "Foo", provider: "Bar")
.delete_integration(consumer: "foo-consumer", provider: "bar-provider")
.create_environment(name: "test")
.create_environment(name: "prod", production: true)
.publish_pact(consumer: "foo-consumer", consumer_version: "1", provider: "bar-provider", content_id: "111", tag: "main")
.get_pacts_for_verification(
enable_pending: true,
provider_version_tag: "main",
include_wip_pacts_since: "2020-01-01",
consumer_version_selectors: [{ tag: "main", latest: true }])
.verify_pact(
index: 0,
provider_version_tag: "main",
provider_version: "1",
success: true
)
.record_deployment(pacticipant: "bar-provider", version: "1", environment_name: "test")
.record_deployment(pacticipant: "bar-provider", version: "1", environment_name: "prod")
.record_deployment(pacticipant: "foo-consumer", version: "1", environment_name: "prod")
.get_pacts_for_verification(
enable_pending: true,
provider_version_tag: "main",
include_wip_pacts_since: "2020-01-01",
consumer_version_selectors: [{ tag: "main", latest: true }])
.verify_pact(
index: 0,
provider_version_tag: "main",
provider_version: "2",
success: true
)
.record_deployment(pacticipant: "bar-provider", version: "2", environment_name: "test")
.create_global_webhook_for_contract_changed(uuid: "7a5da39c-8e50-4cc9-ae16-dfa5be043e8c")
.publish_pact(consumer: "foo-consumer", consumer_version: "2", provider: "bar-provider", content_id: "222", tag: "main")

rescue StandardError => e
puts "#{e.class} #{e.message}"
puts e.backtrace
exit 1
end
39 changes: 35 additions & 4 deletions spec/lib/pact_broker/webhooks/service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,11 @@ module Webhooks
let(:consumer_version) { PactBroker::Domain::Version.new(number: '1.2.3') }
let(:consumer) { PactBroker::Domain::Pacticipant.new(name: 'Consumer') }
let(:provider) { PactBroker::Domain::Pacticipant.new(name: 'Provider') }
let(:webhooks) { [instance_double(PactBroker::Domain::Webhook, description: 'description', uuid: '1244')]}
let(:webhooks) { [webhook]}
let(:webhook) do
instance_double(PactBroker::Domain::Webhook, description: 'description', uuid: '1244', expand_currently_deployed_provider_versions?: expand_currently_deployed)
end
let(:expand_currently_deployed) { false }
let(:triggered_webhook) { instance_double(PactBroker::Webhooks::TriggeredWebhook) }
let(:webhook_execution_configuration) { double('webhook_execution_configuration', webhook_context: webhook_context) }
let(:webhook_context) { { base_url: "http://example.org" } }
Expand Down Expand Up @@ -206,21 +210,48 @@ module Webhooks
end

context "when webhooks are found" do
it "executes the webhook" do
expect(Service).to receive(:run_later).with(webhooks, pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context, options)
it "schedules the webhook" do
expect(Service).to receive(:run_webhooks_later).with(webhooks, pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context, options)
subject
end

it "merges the event name in the options" do
expect(webhook_execution_configuration).to receive(:with_webhook_context).with(event_name: PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED)
subject
end

context "when there should be a webhook triggered for each currently deployed version" do
before do
allow(Service).to receive(:deployed_version_service).and_return(deployed_version_service)
allow(deployed_version_service).to receive(:find_currently_deployed_versions_for_pacticipant).and_return(currently_deployed_versions)
end
let(:expand_currently_deployed) { true }
let(:deployed_version_service) { class_double("PactBroker::Deployments::DeployedVersionService").as_stubbed_const }
let(:currently_deployed_version_1) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "1") }
let(:currently_deployed_version_2) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "2") }
let(:currently_deployed_versions) { [currently_deployed_version_1, currently_deployed_version_2] }

it "schedules a triggered webhook for each currently deployed version" do
expect(Service).to receive(:schedule_webhook).with(webhook, pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context.merge(currently_deployed_provider_version_number: "1"), options, 0)
expect(Service).to receive(:schedule_webhook).with(webhook, pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context.merge(currently_deployed_provider_version_number: "2"), options, 5)
subject
end

context "when the same version is deployed to multiple environments" do
let(:currently_deployed_version_2) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "1") }

it "only triggers one webhook" do
expect(Service).to receive(:schedule_webhook).with(anything, anything, anything, anything, expected_event_context.merge(currently_deployed_provider_version_number: "1"), anything, 0)
subject
end
end
end
end

context "when no webhooks are found" do
let(:webhooks) { [] }
it "does nothing" do
expect(Service).to_not receive(:run_later)
expect(Service).to_not receive(:run_webhooks_later)
subject
end

Expand Down

0 comments on commit 1e5487f

Please sign in to comment.