pact_broker-client 1.13.1 → 1.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -1
- data/CHANGELOG.md +17 -0
- data/README.md +28 -17
- data/doc/markdown/Pact Broker Client - Pact Broker.md +582 -0
- data/doc/markdown/README.md +3 -0
- data/doc/pacts/markdown/Pact Broker Client - Pact Broker.md +1174 -0
- data/doc/pacts/markdown/README.md +3 -0
- data/example/scripts/README.md +13 -0
- data/example/scripts/deploy-consumer.sh +26 -0
- data/example/scripts/pact.json +67 -0
- data/example/scripts/publish-pact.sh +3 -0
- data/example/scripts/publish-verification.sh +3 -0
- data/example/scripts/verification.json +4 -0
- data/lib/pact_broker/client/base_client.rb +17 -1
- data/lib/pact_broker/client/cli/broker.rb +27 -0
- data/lib/pact_broker/client/cli/can_i_deploy_long_desc.txt +13 -3
- data/lib/pact_broker/client/error.rb +2 -0
- data/lib/pact_broker/client/matrix.rb +0 -2
- data/lib/pact_broker/client/version.rb +1 -1
- data/lib/pact_broker/client/versions.rb +15 -5
- data/lib/pact_broker/client/versions/describe.rb +60 -0
- data/lib/pact_broker/client/versions/formatter.rb +20 -0
- data/lib/pact_broker/client/versions/json_formatter.rb +13 -0
- data/lib/pact_broker/client/versions/text_formatter.rb +31 -0
- data/spec/integration/can_i_deploy_spec.rb +2 -2
- data/spec/integration/create_version_tag_spec.rb +1 -1
- data/spec/lib/pact_broker/client/base_client_spec.rb +28 -0
- data/spec/lib/pact_broker/client/versions/describe_spec.rb +65 -0
- data/spec/pacts/pact_broker_client-pact_broker.json +118 -0
- data/spec/service_providers/pact_broker_client_versions_spec.rb +114 -0
- data/spec/service_providers/pact_helper.rb +1 -0
- metadata +19 -3
@@ -0,0 +1,13 @@
|
|
1
|
+
# Example scripts
|
2
|
+
|
3
|
+
These are designed to be run from the top level of the pact_broker-client repository.
|
4
|
+
|
5
|
+
You will need to set your own values for `PACT_BROKER_BASE_URL`, `PACT_BROKER_USERNAME`, and `PACT_BROKER_PASSWORD`
|
6
|
+
|
7
|
+
eg.
|
8
|
+
|
9
|
+
export PACT_BROKER_BASE_URL="http://..."
|
10
|
+
export PACT_BROKER_USERNAME="username"
|
11
|
+
export PACT_BROKER_PASSWORD="password"
|
12
|
+
example/scripts/publish-pact.sh
|
13
|
+
example/scripts/publish-verification.sh
|
@@ -0,0 +1,26 @@
|
|
1
|
+
version="1"
|
2
|
+
stage="development"
|
3
|
+
application="A"
|
4
|
+
|
5
|
+
export PACT_BROKER_CLIENT="bundle exec bin/pact-broker"
|
6
|
+
export PACT_BROKER_BASE_URL="http://localhost:9292"
|
7
|
+
|
8
|
+
can_i_deploy_output=$(${PACT_BROKER_CLIENT} can-i-deploy --pacticipant ${application} --version ${version})
|
9
|
+
can_i_deploy_exit_code=$?
|
10
|
+
|
11
|
+
set -e
|
12
|
+
|
13
|
+
echo "${can_i_deploy_output}"
|
14
|
+
|
15
|
+
if [[ "${can_i_deploy_exit_code}" == "0" ]]; then
|
16
|
+
existing_tags=$(${PACT_BROKER_CLIENT} describe-version --pacticipant ${application} --version ${version} --output json | jq "[._embedded.tags[].name]" | jq 'join("\n")' --raw-output)
|
17
|
+
if [ $(echo "${existing_tags}" | grep "${stage}") ]; then
|
18
|
+
echo "Version ${version} of ${application} has already been deployed to ${stage}"
|
19
|
+
else
|
20
|
+
echo "Deploying version ${version} of ${application} to ${stage}"
|
21
|
+
# do deployment here
|
22
|
+
${PACT_BROKER_CLIENT} create-version-tag --pacticipant ${application} --version ${version} --tag ${stage}
|
23
|
+
fi
|
24
|
+
else
|
25
|
+
echo "Cannot currently deploy version ${version} of ${application} to ${stage}"
|
26
|
+
fi
|
@@ -0,0 +1,67 @@
|
|
1
|
+
{
|
2
|
+
"provider": {
|
3
|
+
"name": "Animal Service"
|
4
|
+
},
|
5
|
+
"consumer": {
|
6
|
+
"name": "Zoo App"
|
7
|
+
},
|
8
|
+
"interactions": [
|
9
|
+
{
|
10
|
+
"description": "a request for an alligator",
|
11
|
+
"provider_state": "there is an alligator named Mary",
|
12
|
+
"request": {
|
13
|
+
"method": "get",
|
14
|
+
"path": "/alligators/Mary",
|
15
|
+
"headers": {
|
16
|
+
"Accept": "application/json"
|
17
|
+
}
|
18
|
+
},
|
19
|
+
"response": {
|
20
|
+
"status": 200,
|
21
|
+
"headers": {
|
22
|
+
"Content-Type": "application/json;charset=utf-8"
|
23
|
+
},
|
24
|
+
"body": {
|
25
|
+
"name": "Mary"
|
26
|
+
}
|
27
|
+
}
|
28
|
+
},
|
29
|
+
{
|
30
|
+
"description": "a request for an alligator",
|
31
|
+
"provider_state": "there is not an alligator named Mary",
|
32
|
+
"request": {
|
33
|
+
"method": "get",
|
34
|
+
"path": "/alligators/Mary",
|
35
|
+
"headers": {
|
36
|
+
"Accept": "application/json"
|
37
|
+
}
|
38
|
+
},
|
39
|
+
"response": {
|
40
|
+
"status": 404
|
41
|
+
}
|
42
|
+
},
|
43
|
+
{
|
44
|
+
"description": "a request for an alligator",
|
45
|
+
"provider_state": "an error occurs retrieving an alligator",
|
46
|
+
"request": {
|
47
|
+
"method": "get",
|
48
|
+
"path": "/alligators/Mary",
|
49
|
+
"headers": {
|
50
|
+
"Accept": "application/json"
|
51
|
+
}
|
52
|
+
},
|
53
|
+
"response": {
|
54
|
+
"status": 500,
|
55
|
+
"headers": {
|
56
|
+
"Content-Type": "application/json;charset=utf-8"
|
57
|
+
},
|
58
|
+
"body": {
|
59
|
+
"error": "Argh!!!"
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|
63
|
+
],
|
64
|
+
"metadata": {
|
65
|
+
"pactSpecificationVersion": "1.0.0"
|
66
|
+
}
|
67
|
+
}
|
@@ -0,0 +1,3 @@
|
|
1
|
+
response_body=$(curl -u ${PACT_BROKER_USERNAME}:${PACT_BROKER_PASSWORD} ${PACT_BROKER_BASE_URL}/pacts/provider/Animal%20Service/consumer/Zoo%20App/latest)
|
2
|
+
verification_url=$(echo "${response_body}" | ruby -e "require 'json'; puts JSON.parse(ARGF.read)['_links']['pb:publish-verification-results']['href']")
|
3
|
+
curl -XPOST -u ${PACT_BROKER_USERNAME}:${PACT_BROKER_PASSWORD} -H 'Content-Type: application/json' -d@$(dirname "$0")/verification.json ${verification_url}
|
@@ -41,6 +41,7 @@ module PactBroker
|
|
41
41
|
@client_options = options[:client_options] || {}
|
42
42
|
@verbose = @client_options[:verbose]
|
43
43
|
self.class.base_uri base_url
|
44
|
+
self.class.debug_output($stderr) if verbose?
|
44
45
|
self.class.basic_auth(client_options[:basic_auth][:username], client_options[:basic_auth][:password]) if client_options[:basic_auth]
|
45
46
|
end
|
46
47
|
|
@@ -62,7 +63,7 @@ module PactBroker
|
|
62
63
|
|
63
64
|
def handle_response response
|
64
65
|
if response.success?
|
65
|
-
yield
|
66
|
+
yield response
|
66
67
|
elsif response.code == 404
|
67
68
|
nil
|
68
69
|
elsif response.code == 401
|
@@ -97,6 +98,21 @@ module PactBroker
|
|
97
98
|
self.class.get(url, *args)
|
98
99
|
end
|
99
100
|
|
101
|
+
def url_for_relation relation_name, params
|
102
|
+
handle_response(get("/", headers: default_get_headers)) do | response |
|
103
|
+
relation = (JSON.parse(response.body)['_links'] || {})[relation_name]
|
104
|
+
if relation
|
105
|
+
url = relation['href']
|
106
|
+
params.each do | (key, value) |
|
107
|
+
url = url.gsub("{#{key}}", value)
|
108
|
+
end
|
109
|
+
url
|
110
|
+
else
|
111
|
+
raise PactBroker::Client::RelationNotFound.new("Could not find relation #{relation_name} in index resource. Try upgrading your Pact Broker as the feature you require may not exist in your version.")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
100
116
|
def verbose?
|
101
117
|
@verbose
|
102
118
|
end
|
@@ -7,6 +7,7 @@ require 'pact_broker/client/publish_pacts'
|
|
7
7
|
require 'rake/file_list'
|
8
8
|
require 'thor/error'
|
9
9
|
require 'pact_broker/client/create_tag'
|
10
|
+
require 'pact_broker/client/versions/describe'
|
10
11
|
|
11
12
|
module PactBroker
|
12
13
|
module Client
|
@@ -72,6 +73,32 @@ module PactBroker
|
|
72
73
|
pact_broker_client_options)
|
73
74
|
end
|
74
75
|
|
76
|
+
method_option :pacticipant, required: true, aliases: "-a", desc: "The name of the pacticipant that the version belongs to."
|
77
|
+
method_option :version, required: false, aliases: "-e", desc: "The pacticipant version number."
|
78
|
+
method_option :latest, required: false, aliases: "-l", banner: '[TAG]', desc: "Describe the latest pacticipant version. Optionally specify a TAG to describe the latest version with the specified tag."
|
79
|
+
method_option :broker_base_url, required: true, aliases: "-b", desc: "The base URL of the Pact Broker"
|
80
|
+
method_option :broker_username, aliases: "-u", desc: "Pact Broker basic auth username"
|
81
|
+
method_option :broker_password, aliases: "-p", desc: "Pact Broker basic auth password"
|
82
|
+
method_option :output, aliases: "-o", desc: "json or table or id", default: 'table'
|
83
|
+
method_option :verbose, aliases: "-v", type: :boolean, default: false, required: false, desc: "Verbose output. Default: false"
|
84
|
+
|
85
|
+
desc 'describe-version', 'Describes a pacticipant version. If no version or tag is specified, the latest version is described.'
|
86
|
+
def describe_version
|
87
|
+
latest = !options.latest.nil? || (options.latest.nil? && options.version.nil?)
|
88
|
+
params = {
|
89
|
+
pacticipant: options.pacticipant,
|
90
|
+
version: options.version,
|
91
|
+
latest: latest,
|
92
|
+
tag: options.latest != "latest" ? options.latest : nil
|
93
|
+
}
|
94
|
+
opts = {
|
95
|
+
output: options.output
|
96
|
+
}
|
97
|
+
result = PactBroker::Client::Versions::Describe.call(params, opts, options.broker_base_url, pact_broker_client_options)
|
98
|
+
$stdout.puts result.message
|
99
|
+
exit(1) unless result.success
|
100
|
+
end
|
101
|
+
|
75
102
|
desc 'version', "Show the pact_broker-client gem version"
|
76
103
|
def version
|
77
104
|
$stdout.puts PactBroker::Client::VERSION
|
@@ -1,20 +1,30 @@
|
|
1
1
|
Returns exit code 0 or 1, indicating whether or not the specified pacticipant versions are compatible. Prints out the relevant pact/verification details.
|
2
2
|
|
3
|
-
WARNING! This feature is in beta release, and backwards compatibility is NOT guaranteed. You will need the latest version of the Pact Broker for this feature to work.
|
4
|
-
|
5
3
|
The environment variables PACT_BROKER_BASE_URL_BASE_URL, PACT_BROKER_BASE_URL_USERNAME and PACT_BROKER_BASE_URL_PASSWORD may be used instead of their respective command line options.
|
6
4
|
|
7
5
|
SCENARIOS
|
8
6
|
|
7
|
+
# If every build goes straight to production
|
8
|
+
|
9
9
|
Check the status of the pacts for a pacticipant version. Note that this only checks that the most recent verification for each pact is successful. It doesn't provide any assurance that the pact has been verified by the *production* version of the provider, however, it is sufficient if you are doing true continuous deployment.
|
10
10
|
|
11
11
|
$ pact-broker can-i-deploy --pacticipant PACTICIPANT --version VERSION --broker-base-url BROKER_BASE_URL
|
12
12
|
|
13
|
+
# If every build does NOT go straight to production
|
14
|
+
|
15
|
+
## Recommended approach
|
16
|
+
|
13
17
|
If all applications within the pact network are not being deployed continuously (ie. if there is a gap between pact verification and actual deployment) then the following strategy is recommended. Each application version should be tagged in the broker with the name of the stage (eg. test, staging, production) as it is deployed (see the pact-broker create-version-tag CLI). This enables you to use the following very simple command to check if the application version you are about to deploy is compatible with every other application version already deployed in that environment.
|
14
18
|
|
15
19
|
$ pact-broker can-i-deploy --pacticipant PACTICIPANT --version VERSION --to TAG --broker-base-url BROKER_BASE_URL
|
16
20
|
|
17
|
-
|
21
|
+
## Other approaches
|
22
|
+
|
23
|
+
If you do not/cannot tag every application at deployment, you have two options. You can either use the very first form of this command which just checks that the *latest* verification is successful (not recommended as it's the production version that you really care about) or you will need to determine the production versions of each collaborating application from some other source (eg. git) and explictly reference each one using one using the format `--pacticipant PACTICIPANT1 --version VERSION1 --pacticipant PACTICIPANT2 --version VERSION2 ...`
|
24
|
+
|
25
|
+
# Other commands
|
26
|
+
|
27
|
+
Check the status of the pacts for the latest pacticipant version. This form is not recommended for use in your CI as it is possible that the version you are about to deploy is not the the version that the Broker considers the latest. It's best to specify the version explictly.
|
18
28
|
|
19
29
|
$ pact-broker can-i-deploy --pacticipant PACTICIPANT --latest --broker-base-url BROKER_BASE_URL
|
20
30
|
|
@@ -10,8 +10,6 @@ module PactBroker
|
|
10
10
|
latestby: latestby
|
11
11
|
}.merge(query_options(options))
|
12
12
|
response = self.class.get("/matrix", query: query, headers: default_get_headers)
|
13
|
-
$stdout.puts("DEBUG: Response headers #{response.headers}") if verbose?
|
14
|
-
$stdout.puts("DEBUG: Response body #{response}") if verbose?
|
15
13
|
response = handle_response(response) do
|
16
14
|
JSON.parse(response.body, symbolize_names: true)
|
17
15
|
end
|
@@ -5,13 +5,23 @@ module PactBroker
|
|
5
5
|
module Client
|
6
6
|
class Versions < BaseClient
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
query = options[:tag] ? {tag: options[:tag]} : {}
|
11
|
-
response = self.class.get("#{versions_base_url(options)}/latest", query: query, headers: default_get_headers)
|
8
|
+
def find options
|
9
|
+
response = get("#{version_base_url(options)}", headers: default_get_headers)
|
12
10
|
|
13
11
|
handle_response(response) do
|
14
|
-
|
12
|
+
JSON.parse(response.body, symbolize_names: true)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def latest options
|
17
|
+
puts options
|
18
|
+
url = if options[:tag]
|
19
|
+
url_for_relation('pb:latest-tagged-version', pacticipant: options.fetch(:pacticipant), tag: options.fetch(:tag))
|
20
|
+
else
|
21
|
+
url_for_relation('pb:latest-version', pacticipant: options.fetch(:pacticipant))
|
22
|
+
end
|
23
|
+
handle_response(get(url, headers: default_get_headers)) do | response |
|
24
|
+
JSON.parse(response.body, symbolize_names: true)
|
15
25
|
end
|
16
26
|
end
|
17
27
|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'pact_broker/client/pact_broker_client'
|
2
|
+
require 'pact_broker/client/versions/formatter'
|
3
|
+
|
4
|
+
module PactBroker
|
5
|
+
module Client
|
6
|
+
class Versions
|
7
|
+
class Describe
|
8
|
+
|
9
|
+
class Result
|
10
|
+
attr_reader :success, :message
|
11
|
+
|
12
|
+
def initialize success, message = nil
|
13
|
+
@success = success
|
14
|
+
@message = message
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.call params, options, pact_broker_base_url, pact_broker_client_options
|
19
|
+
new(params, options, pact_broker_base_url, pact_broker_client_options).call
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize params, options, pact_broker_base_url, pact_broker_client_options
|
23
|
+
@params = params
|
24
|
+
@options = options
|
25
|
+
@pact_broker_base_url = pact_broker_base_url
|
26
|
+
@pact_broker_client_options = pact_broker_client_options
|
27
|
+
end
|
28
|
+
|
29
|
+
def call
|
30
|
+
version_hash = if params[:version]
|
31
|
+
versions_client.find params
|
32
|
+
else
|
33
|
+
pact_broker_client.pacticipants.versions.latest(params)
|
34
|
+
end
|
35
|
+
if version_hash
|
36
|
+
Result.new(true, format_version(version_hash))
|
37
|
+
else
|
38
|
+
Result.new(false, "Pacticipant version not found")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def format_version(version_hash)
|
45
|
+
Formatter.call(version_hash, options[:output])
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :params, :options, :pact_broker_base_url, :pact_broker_client_options
|
49
|
+
|
50
|
+
def versions_client
|
51
|
+
pact_broker_client.pacticipants.versions
|
52
|
+
end
|
53
|
+
|
54
|
+
def pact_broker_client
|
55
|
+
@pact_broker_client ||= PactBroker::Client::PactBrokerClient.new(base_url: pact_broker_base_url, client_options: pact_broker_client_options)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'pact_broker/client/versions/json_formatter'
|
2
|
+
require 'pact_broker/client/versions/text_formatter'
|
3
|
+
|
4
|
+
module PactBroker
|
5
|
+
module Client
|
6
|
+
class Versions
|
7
|
+
class Formatter
|
8
|
+
def self.call(matrix_lines, format)
|
9
|
+
formatter = case format
|
10
|
+
when 'json' then JsonFormatter
|
11
|
+
when 'table' then TextFormatter
|
12
|
+
else
|
13
|
+
raise PactBroker::Client::Error.new("Invalid output option '#{format}. Must be one of 'table' or 'json'.")
|
14
|
+
end
|
15
|
+
formatter.call(matrix_lines)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'table_print'
|
2
|
+
|
3
|
+
module PactBroker
|
4
|
+
module Client
|
5
|
+
class Versions
|
6
|
+
class TextFormatter
|
7
|
+
|
8
|
+
Line = Struct.new(:number, :tags)
|
9
|
+
|
10
|
+
OPTIONS = [
|
11
|
+
{ number: {} },
|
12
|
+
{ tags: {} }
|
13
|
+
]
|
14
|
+
|
15
|
+
def self.call(version_hash)
|
16
|
+
tags = (lookup(version_hash, [], :_embedded, :tags) || []).collect{ | t| t[:name] }.join(" ")
|
17
|
+
data = Line.new(version_hash[:number], tags)
|
18
|
+
|
19
|
+
printer = TablePrint::Printer.new([data], OPTIONS)
|
20
|
+
printer.table_print
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.lookup line, default, *keys
|
24
|
+
keys.reduce(line) { | line, key | line[key] }
|
25
|
+
rescue NoMethodError
|
26
|
+
default
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -5,7 +5,7 @@ describe "pact-broker can-i-deploy" do
|
|
5
5
|
end
|
6
6
|
|
7
7
|
context "when the pacticipants can be deployed" do
|
8
|
-
subject { `bundle exec bin/pact-broker can-i-deploy
|
8
|
+
subject { `bundle exec bin/pact-broker can-i-deploy --pacticipant Foo --version 1.2.3 --pacticipant Bar --version 4.5.6 --broker-base-url http://localhost:5000` }
|
9
9
|
|
10
10
|
it "returns a success exit code" do
|
11
11
|
subject
|
@@ -18,7 +18,7 @@ describe "pact-broker can-i-deploy" do
|
|
18
18
|
end
|
19
19
|
|
20
20
|
context "when the pacticipants can't be deployed" do
|
21
|
-
subject { `bundle exec bin/pact-broker can-i-deploy
|
21
|
+
subject { `bundle exec bin/pact-broker can-i-deploy --pacticipant Wiffle --version 1.2.3 --pacticipant Meep --version 4.5.6 --broker-base-url http://localhost:5000` }
|
22
22
|
|
23
23
|
it "returns an error exit code" do
|
24
24
|
subject
|