pact_broker 2.109.1 → 2.111.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/docs/developer/design_pattern_for_eager_loading_collections.md +23 -0
  4. data/docs/developer/matrix.md +95 -0
  5. data/docs/developer/rack.md +11 -0
  6. data/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb +1 -1
  7. data/lib/pact_broker/api/decorators/base_decorator.rb +10 -1
  8. data/lib/pact_broker/api/decorators/label_decorator.rb +19 -13
  9. data/lib/pact_broker/api/decorators/labels_decorator.rb +23 -0
  10. data/lib/pact_broker/api/decorators/pacticipant_decorator.rb +1 -1
  11. data/lib/pact_broker/api/decorators/version_decorator.rb +14 -0
  12. data/lib/pact_broker/api/paths.rb +2 -1
  13. data/lib/pact_broker/api/resources/branch_versions.rb +1 -1
  14. data/lib/pact_broker/api/resources/can_i_merge_badge.rb +36 -0
  15. data/lib/pact_broker/api/resources/labels.rb +37 -0
  16. data/lib/pact_broker/api/resources/provider_pacts_for_verification.rb +5 -0
  17. data/lib/pact_broker/api/resources/versions.rb +1 -1
  18. data/lib/pact_broker/api.rb +4 -0
  19. data/lib/pact_broker/app.rb +24 -9
  20. data/lib/pact_broker/badges/service.rb +18 -0
  21. data/lib/pact_broker/contracts/service.rb +6 -2
  22. data/lib/pact_broker/db/advisory_lock.rb +58 -0
  23. data/lib/pact_broker/domain/version.rb +3 -2
  24. data/lib/pact_broker/integrations/integration.rb +11 -1
  25. data/lib/pact_broker/integrations/repository.rb +46 -7
  26. data/lib/pact_broker/integrations/service.rb +2 -0
  27. data/lib/pact_broker/labels/repository.rb +5 -0
  28. data/lib/pact_broker/labels/service.rb +4 -0
  29. data/lib/pact_broker/matrix/service.rb +26 -0
  30. data/lib/pact_broker/pacticipants/repository.rb +10 -1
  31. data/lib/pact_broker/pacts/pact_publication_dataset_module.rb +56 -3
  32. data/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb +1 -0
  33. data/lib/pact_broker/pacts/pacts_for_verification_repository.rb +4 -4
  34. data/lib/pact_broker/pacts/repository.rb +2 -2
  35. data/lib/pact_broker/pacts/selector.rb +8 -2
  36. data/lib/pact_broker/tasks/clean_task.rb +68 -26
  37. data/lib/pact_broker/test/test_data_builder.rb +17 -5
  38. data/lib/pact_broker/version.rb +1 -1
  39. data/lib/pact_broker/versions/repository.rb +2 -20
  40. data/lib/pact_broker/versions/service.rb +2 -6
  41. data/lib/pact_broker/webhooks/execution.rb +8 -8
  42. data/lib/sequel/extensions/pg_advisory_lock.rb +101 -0
  43. data/pact_broker.gemspec +1 -1
  44. metadata +13 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4c49978b4312049fd2e394ce46bcf90f64125ff50556f9bb01f9610e1310546
4
- data.tar.gz: 43f24ab98f5ce7bd757a52a085d653668d83395b6889921e8dc0d4aac5982b73
3
+ metadata.gz: e21e3c2fc82cfe4697582921613bd435e026cf6f381c64c33d703b8c31c586be
4
+ data.tar.gz: 6b6bc75a486f936441b172c7a80a7e8541f3bbb1731be03b1e33a869327014bf
5
5
  SHA512:
6
- metadata.gz: 3211f2cc4b2e9fbae7dd8a07bd1c49dc9d672fa46f6f4c83b6652b2266dc9498d4858a9a7714f91b09914d872e522affc5e96753cf3317e39228fecc620404c1
7
- data.tar.gz: 36a5bc3f3ecc827cd6e24fa21b4d4bc9f943ec75bfdb3db8cdcdd1399533739bf0a2c15c68df816ae995fa58987fa43d4da45dcb72a2d68fff12f7594a69906a
6
+ metadata.gz: 2afe779170a4910f06ace0a29b8ee66fa81bc355fc1a1c22a532fb3f9f64f5775309543c23b3b7ae4c4c837c1a66d0b10ddb0f62e8e9f586b7fd4942dc29d33c
7
+ data.tar.gz: 3b2738f3785dc1c272d4e1ba24104b4786bd467780808d41bdf2dd7f2ce0e83f0f11645a8ca3b8fd16642055a897d60b573ea7fc8392751cf0a59ecafeba6130
data/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ <a name="v2.111.0"></a>
2
+ ### v2.111.0 (2024-07-26)
3
+
4
+ #### Features
5
+
6
+ * add new label api (#703) ([ff3f84e2](/../../commit/ff3f84e2))
7
+ * search pacticipants by display_name ([c5945801](/../../commit/c5945801))
8
+
9
+ #### Bug Fixes
10
+
11
+ * **docs**
12
+ * Update OAS with correct ref to Notice schema ([6729b7f8](/../../commit/6729b7f8))
13
+
14
+ <a name="v2.110.0"></a>
15
+ ### v2.110.0 (2024-04-02)
16
+
17
+ #### Features
18
+
19
+ * reduce contention when updating the contract_data_updated_at field for integrations (#671) ([ff72d03c](/../../commit/ff72d03c))
20
+ * support consumer version selector for all branches (#667) ([34334ca8](/../../commit/34334ca8))
21
+
22
+ * **clean**
23
+ * use postgres advisory locks to ensure only one process can run a clean at a time (#672) ([637c25fa](/../../commit/637c25fa))
24
+
25
+ #### Bug Fixes
26
+
27
+ * use for_all_tag_heads instead of latest_by_consumer_tag when fetching wip by branch ([14148a34](/../../commit/14148a34))
28
+ * optimise WIP pacts by using branch/tag heads (#668) ([871209e1](/../../commit/871209e1))
29
+ * improve performance of WIP pacts by using branch heads instead of calculating latest pact for branch ([f9705583](/../../commit/f9705583))
30
+
1
31
  <a name="v2.109.1"></a>
2
32
  ### v2.109.1 (2024-02-21)
3
33
 
@@ -0,0 +1,23 @@
1
+ # Design pattern for eager loading collections
2
+
3
+ For collection resources (eg. `/versions` ), associations included in the items (eg. branch versions) must be eager loaded for performance reasons.
4
+
5
+ The responsiblities of each class used to render a collection resource are as follows:
6
+
7
+ * collection decorator (eg. `VersionsDecorator`) - delegate each item in the collection to be rendered by the decorator for the individual item, render pagination links
8
+ * item decorator (eg. `VersionDecorator`) - render the JSON for each item
9
+ * resource (eg. `PactBroker::Api::Resources::Versions`) - coordinate between, and delegate to, the service and the decorator
10
+ * service (eg. `PactBroker::Versions::Service`) - just delegate to repository, as there is no business logic required
11
+ * repository (eg. `PactBroker::Versions::Repository`) - load the domain objects from the database
12
+
13
+ If the associations for a model are not eager loaded, then each individual association will be lazy loaded when the decorator for the item calls the association method to render it. This results in at least `<number of items in the collection> * <number of associations to render>` calls to the database, and potentially more if any of the associations have their own associations that are required to render the item. This can cause significant performance issues.
14
+
15
+ To efficiently render a collection resource, associations must be eager loaded when the collection items are loaded from the database in the repository. Since the repository method for loading the collection may be used in multiple places, and the eager loaded associations required for each of those places may be different (some may not require any associations to be eager loaded), we do not want to hard code the repository to load a fixed set of associations. The list of associations to eager load is therefore passed in to the repository finder method as an argument `eager_load_associations`.
16
+
17
+ The decorator is the class that knows what associations are going to be called on the model to render the JSON, so following the design guideline of "put things together that change together", the best place for the declaration of "what associations should be eager loaded for this decorator" is in the decorator itself. The `PactBroker::Api::Decorators::BaseDecorator` has a default implementation of this method called `eager_load_associations` which attempts to automatically identify the required associations, but this can be overridden when necessary.
18
+
19
+ We can therefore add the following responsiblities to our previous list:
20
+
21
+ * item decorator - return a list of all the associations (including nested associations) that should be eager loaded in order to render its item
22
+ * repository - eager load the associations that have been passed into it
23
+ * resource - pass in the eager load associations to the repository from the decorator
@@ -0,0 +1,95 @@
1
+ # The Matrix
2
+
3
+ Read [these docs](https://docs.pact.io/pact_broker/advanced_topics/matrix_selectors) first for a introduction to matrix selectors and options from a user's perspective.
4
+
5
+ ## Terminology
6
+
7
+ ### Selectors
8
+
9
+ #### Selector classes
10
+
11
+ * `Unresolved selectors` - `PactBroker::Matrix::UnresolvedSelector` the class of the specified and ignore selectors, as they are created directly from the HTTP query. At this point, they are "unresolved" in that we do not know the IDs of the pacticipants/versions that they represent, and whether or not they even exist.
12
+
13
+ * `Resolved selectors` - `PactBroker::Matrix::ResolvedSelector` the class of the object created for each selector, after its pacticipant and version IDs have been identified. If a selector represents multiple pacticipant versions (eg `{ branch: "main" }`) then one `ResolvedSelector` object will be returned for each pacticipant version found. The resolved selector keeps a reference to its original unresolved selector, as well as the pacticipant ID and the version ID. If the version or pacticipant specified does not actually exist, then a resolved selector is returned that indicates this.
14
+
15
+ #### Selector collections
16
+
17
+ * `Specified selectors` - the collection of objects that describe the application version(s) specified explicitly in the matrix query. eg. in `can-i-deploy --pacticipant Foo --version 1` the specified selector is `PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: "Foo", pacticipant_version_number: "1")`. There may be multiple specified selectors, for example, in the Matrix page for an integration Foo/Bar, the specified selectors would be `[ UnresolvedSelector.new(pacticipant_name: "Foo"), UnresolvedSelector.new(pacticipant_name: "Bar")]`. The selectors may use various combinations of pacticipant name, version number, branch, tag or environment to identify the pacticipant versions.
18
+
19
+ * `Ignore selectors` - the collection of objects that describe the applications (or application versions) to ignore, if there are any missing or failed verifications between the versions for the specified selectors and the ignore selectors. eg. If we know that provider Dog is not ready and the verifications are failing, but the integration is feature toggled off in the Foo code, we would use the command `can-i-deploy --pacticipant Foo --version 1 --to-environment production --ignore Dog` to allow `can-i-deploy` to pass. An ignore selector can have a version number, but it's more common to just provide the name.
20
+
21
+ * `Inferred selectors` - the collection of objects that describe the application versions(s) that already exist in the target environment/with the target tag/on the target branches. These are identified during the matrix query when the `can-i-deploy` query has a `--to TAG`/`--to-environment`/`--with-main-branches` option specified, and they allow us to find the full set of matrix rows that tell us whether or the application version we're about to deploy is compatible with its integrated applications. For example, given Foo v1 is a consumer of Bar, and Bar v8 is in production, and the `can-i-deploy` command is `can-i-deploy --pacticipant Foo --version 1 --to-environment production`, then an inferred unresolved selector is created for Bar in the production environment (`UnresolvedSelector.new(pacticipant_name: "Bar", environment_name: "production")`) which is then resolved to Bar v8.
22
+
23
+ #### Notes on selector classes/collections
24
+
25
+ Specified, ignore, and inferred selectors all start life as `UnresolvedSelector` objects, which then get resolved into `ResolvedSelector` objects.
26
+
27
+ ### Options
28
+
29
+ TBC.
30
+
31
+ ## How the matrix query works
32
+
33
+ 1. Given that Foo v1 is a consumer of Bar, and Bar v8 is currently in production, take the pact-broker CLI command `can-i-deploy --pacticipant Foo --version 1 --to-environment production --ignore Dog`
34
+
35
+ 1. Turn that into a query string and make a request to the `/matrix` endpoint.
36
+
37
+ 1. Parse the HTTP query into the specified selectors, the ignore selectors and options.
38
+
39
+ * specified selectors - `PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: "Foo", pacticipant_version_number: "1")`
40
+ * ignore selectors - `PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: "Dog")`
41
+ * options: `{ to_environment: "production" }`
42
+
43
+ 1. Validate the selectors.
44
+
45
+ * Ensure conflicting fields are not used.
46
+ * Return an error if any of the pacticipants in the specified selectors or the environment do not exist.
47
+ * Do not check for the existance of version numbers or tags, or pacticipants in the ignore selectors.
48
+
49
+ 1. "Resolve" the ignore selectors (find the pacticipant and version IDs for the selectors).
50
+
51
+ 1. "Resolve" the specified selectors, passing in the ignore selectors to identify whether or not the resolved selector should be "ignored".
52
+
53
+ 1. Identify the integrations that the versions in the specified selectors are involved in.
54
+
55
+ * Identify the providers of the consumers: for the versions of each specified selector, find any pacts created for that version. If any exist, that means
56
+ the provider for each pact MUST be deployed to the target environment, and the deployed version must have a successful verification with the specified consumer version for the consumer to be safe to deploy. The providers must be identified for the specific consumer *version* of the selector, not just *consumer*, as different versions of a consumer may have different providers as integrations are created and removed over time. This is why we cannot just query the integrations table.
57
+
58
+ Represent the integration by creating a `PactBroker::Matrix::Integration` object for each consumer/provider pair, and mark it as `required: true`.
59
+
60
+ * Identify the consumers of the providers: for the pacticipant of each specified selector, find any integrations in the integrations table where
61
+ the specified pacticipant is the provider. Because specific provider versions don't have a dependency on any specific consumer versions being already present in an environment, we do not need to run this query at the pacticipant version level - we can just check the presence of any consumers at the integration level.
62
+
63
+ Represent the integration by creating a `PactBroker::Matrix::Integration` object for each consumer/provider pair, and mark it as `required: false`.
64
+
65
+ 1. Identify the inferred unresolved selectors (the selectors for the pacticipant versions that are already in the target scope)
66
+
67
+ * Collect all the pacticipant names from the integrations identified in the previous step. For every pacticipant name that does NOT already have a specified selector, create a new unresolved "inferred" selector for that pacticipant, and set the version selection attributes to be the target scope from the original `can-i-deploy` query. eg. Given we have identifed the required `Integration` for consumer Foo and provider Bar, and we are determining if we can deploy Foo to production, create an unresolved selector for Bar in the production environment (`UnresolvedSelector.new(pacticipant_name: "Bar", environment_name: "production"`).
68
+
69
+ 1. "Resolve" the inferred selectors (find the pacticipant and version IDs).
70
+
71
+ 1. Add the specified and inferred resolved selectors together to make the final collection of selectors that will be used to query the matrix.
72
+
73
+ 1. Peform the matrix query.
74
+
75
+ * When there are 2 or more total resolved selectors (the usual usecase):
76
+
77
+ * Create a collection of all the pacticipants in the selectors (let's call it `all_pacticipants_in_selectors`).
78
+
79
+ * Create the pact/consumer version dataset
80
+
81
+ * For each selector, find the pact publications where the consumer version is one of the versions described by the selector, and the provider is one of `all_pacticipants_in_selectors`.
82
+
83
+ * Create the verification/provider version dataset.
84
+
85
+ * For each selector, find the verifications where the provider version is one of the versions described by the selector, and the consumer is one of `all_pacticipants_in_selectors`.
86
+
87
+ * Join the pact/consumer verison dataset to the verification/provider version dataset.
88
+
89
+ * The two datasets are joined on the `pact_version_id` - this is the ID of the unique pact content that every pact publication and provider verification has a reference to. A left outer join is used, so that if there is a pact that doesn't have a verification, a row is present for the pact, with empty provider version columns. This allows us to identify that there is a missing verification.
90
+
91
+
92
+ * When there is only 1 total resolved selector: this is an unusual usecase, and cannot be done via the UI or the CLI, so I'm not going to spend time documenting it. Just know it is supported and theoretically possible.
93
+
94
+
95
+
@@ -0,0 +1,11 @@
1
+ # Rack
2
+
3
+ https://medium.com/quick-code/rack-middleware-vs-rack-application-vs-rack-the-gem-vs-rack-the-architecture-912cd583ed24
4
+ https://github.com/rack/rack/blob/main/SPEC.rdoc
5
+ https://www.rubyguides.com/2018/09/rack-middleware/
6
+
7
+
8
+ * Responds to `call`
9
+ * Accepts a hash of parameters
10
+ * Returns an array where the first item is the http status, the second is a hash of headers, and the third is an object that responds to `each` (or `call`) that provides the body (99% of the time it's an array of length 1 with a string)
11
+
@@ -13,7 +13,7 @@ module PactBroker
13
13
  json do
14
14
  optional(:mainBranch).filled(included_in?: [true])
15
15
  optional(:tag).filled(:str?)
16
- optional(:branch).filled(:str?)
16
+ optional(:branch).filled { str? | eql?(true) }
17
17
  optional(:matchingBranch).filled(included_in?: [true])
18
18
  optional(:latest).filled(included_in?: [true, false])
19
19
  optional(:fallbackTag).filled(:str?)
@@ -34,7 +34,16 @@ module PactBroker
34
34
  end
35
35
  end
36
36
 
37
- # Returns the names of the model associations to eager load for use with this decorator
37
+ # Returns the names of the model associations to eager load for use with this decorator.
38
+ # The default implementation attempts to do an "auto detect" of the associations.
39
+ # For single item decorators, it attempts to identify the attributes that are themselves models.
40
+ # For collection decorators, it delegates to the eager_load_associations
41
+ # method of the single item decorator used to decorate the collection.
42
+ #
43
+ # The "auto detect" logic can only go so far. It cannot identify when a child object needs its own
44
+ # child object(s) to render the attribute.
45
+ # This method should be overridden when the "auto detect" logic cannot identify the correct associations
46
+ # to load. eg VersionDecorator
38
47
  # @return [Array<Symbol>]
39
48
  def self.eager_load_associations
40
49
  if is_collection_resource?
@@ -11,20 +11,26 @@ module PactBroker
11
11
 
12
12
  include Timestamps
13
13
 
14
- link :self do | options |
15
- {
16
- title: "Label",
17
- name: represented.name,
18
- href: label_url(represented, options[:base_url])
19
- }
20
- end
14
+ # This method is overridden to conditionally render the links based on the user_options
15
+ def to_hash(options)
16
+ hash = super
17
+
18
+ unless options.dig(:user_options, :hide_label_decorator_links)
19
+ hash[:_links] = {
20
+ self: {
21
+ title: "Label",
22
+ name: represented.name,
23
+ href: label_url(represented, options.dig(:user_options, :base_url))
24
+ },
25
+ pacticipant: {
26
+ title: "Pacticipant",
27
+ name: represented.pacticipant.name,
28
+ href: pacticipant_url(options.dig(:user_options, :base_url), represented.pacticipant)
29
+ }
30
+ }
31
+ end
21
32
 
22
- link :pacticipant do | options |
23
- {
24
- title: "Pacticipant",
25
- name: represented.pacticipant.name,
26
- href: pacticipant_url(options.fetch(:base_url), represented.pacticipant)
27
- }
33
+ hash
28
34
  end
29
35
  end
30
36
  end
@@ -0,0 +1,23 @@
1
+ require_relative "base_decorator"
2
+ require_relative "pagination_links"
3
+ require_relative "label_decorator"
4
+ require "pact_broker/domain/label"
5
+
6
+ module PactBroker
7
+ module Api
8
+ module Decorators
9
+ class LabelsDecorator < BaseDecorator
10
+ collection :entries, :as => :labels, :class => PactBroker::Domain::Label, :extend => PactBroker::Api::Decorators::LabelDecorator, embedded: true
11
+
12
+ include PaginationLinks
13
+
14
+ link :self do | options |
15
+ {
16
+ title: "Labels",
17
+ href: options.fetch(:resource_url)
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -20,7 +20,7 @@ module PactBroker
20
20
  property :main_branch
21
21
 
22
22
  property :latest_version, as: :latestVersion, :class => PactBroker::Domain::Version, extend: PactBroker::Api::Decorators::EmbeddedVersionDecorator, embedded: true, writeable: false
23
- collection :labels, :class => PactBroker::Domain::Label, extend: PactBroker::Api::Decorators::EmbeddedLabelDecorator, embedded: true
23
+ collection :labels, :class => PactBroker::Domain::Label, extend: PactBroker::Api::Decorators::EmbeddedLabelDecorator, embedded: true, writeable: false
24
24
 
25
25
  include Timestamps
26
26
 
@@ -17,6 +17,20 @@ module PactBroker
17
17
 
18
18
  include Timestamps
19
19
 
20
+ # Returns the list of associations that must be eager loaded to efficiently render a version
21
+ # when this decorator is used in a collection (eg. VersionsDecorator)
22
+ # The associations that need to be eager loaded for the VersionDecorator
23
+ # are hand coded
24
+ # @return <Array>
25
+ def self.eager_load_associations
26
+ [
27
+ :pacticipant,
28
+ :pact_publications,
29
+ { branch_versions: [:version, :branch_head, { branch: :pacticipant }] },
30
+ { tags: :head_tag }
31
+ ]
32
+ end
33
+
20
34
  link :self do | options |
21
35
  {
22
36
  title: "Version",
@@ -3,11 +3,12 @@ module PactBroker
3
3
  module Paths
4
4
  PACT_BADGE_PATH = %r{^/pacts/provider/[^/]+/consumer/.*/badge(?:\.[A-Za-z]+)?$}.freeze
5
5
  MATRIX_BADGE_PATH = %r{^/matrix/provider/[^/]+/latest/[^/]+/consumer/[^/]+/latest/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze
6
+ CAN_I_MERGE_BADGE_PATH = %r{^/pacticipants/[^/]+/main-branch/can-i-merge/badge(?:\.[A-Za-z]+)?$}.freeze
6
7
  CAN_I_DEPLOY_TAG_BADGE_PATH = %r{^/pacticipants/[^/]+/latest-version/[^/]+/can-i-deploy/to/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze
7
8
  CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH = %r{^/pacticipants/[^/]+/branches/[^/]+/latest-version/can-i-deploy/to-environment/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze
8
9
  VERIFICATION_RESULTS = %r{^/pacts/provider/[^/]+/consumer/[^/]+/pact-version/[^/]+/verification-results/[^/]+}
9
10
 
10
- BADGE_PATHS = [PACT_BADGE_PATH, MATRIX_BADGE_PATH, CAN_I_DEPLOY_TAG_BADGE_PATH, CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH]
11
+ BADGE_PATHS = [PACT_BADGE_PATH, MATRIX_BADGE_PATH, CAN_I_DEPLOY_TAG_BADGE_PATH, CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH, CAN_I_MERGE_BADGE_PATH]
11
12
 
12
13
  extend self
13
14
 
@@ -31,7 +31,7 @@ module PactBroker
31
31
  end
32
32
 
33
33
  def versions
34
- @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, { branch_name: identifier_from_path[:branch_name] }, pagination_options)
34
+ @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, { branch_name: identifier_from_path[:branch_name] }, pagination_options, decorator_class(:versions_decorator).eager_load_associations)
35
35
  end
36
36
 
37
37
  def policy_name
@@ -0,0 +1,36 @@
1
+ require "pact_broker/api/resources/base_resource"
2
+ require "pact_broker/api/resources/badge_methods"
3
+
4
+ module PactBroker
5
+ module Api
6
+ module Resources
7
+ class CanIMergeBadge < BaseResource
8
+ include BadgeMethods # This module contains all necessary webmachine methods for badge implementation
9
+
10
+ def badge_url
11
+ if pacticipant.nil? # pacticipant method is defined in BaseResource
12
+ # if the pacticipant is nil, we return an error badge url
13
+ badge_service.error_badge_url("pacticipant", "not found")
14
+ elsif version.nil?
15
+ # when there is no main branch version, we return an error badge url
16
+ badge_service.error_badge_url("main branch version", "not found")
17
+ else
18
+ # we call badge_service to build the badge url
19
+ badge_service.can_i_merge_badge_url(deployable: results)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def results
26
+ # can_i_merge returns true or false if the main branch version is compatible with all the integrations
27
+ @results ||= matrix_service.can_i_merge(pacticipant: pacticipant, latest_main_branch_version: version)
28
+ end
29
+
30
+ def version
31
+ @version ||= version_service.find_latest_version_from_main_branch(pacticipant)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,37 @@
1
+ require "pact_broker/api/resources/base_resource"
2
+ require "pact_broker/api/decorators/labels_decorator"
3
+ require "pact_broker/api/resources/pagination_methods"
4
+
5
+ module PactBroker
6
+ module Api
7
+ module Resources
8
+ class Labels < BaseResource
9
+ include PaginationMethods
10
+
11
+ def content_types_provided
12
+ [["application/hal+json", :to_json]]
13
+ end
14
+
15
+ def allowed_methods
16
+ ["GET", "OPTIONS"]
17
+ end
18
+
19
+ def policy_name
20
+ :'labels::labels'
21
+ end
22
+
23
+ def to_json
24
+ decorator_class(:labels_decorator).new(labels).to_json(
25
+ **decorator_options(
26
+ hide_label_decorator_links: true,
27
+ )
28
+ )
29
+ end
30
+
31
+ def labels
32
+ label_service.get_all_unique_labels(pagination_options)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -15,6 +15,10 @@ module PactBroker
15
15
  [["application/hal+json", :to_json]]
16
16
  end
17
17
 
18
+ # TODO drop support for GET in next major version.
19
+ # GET was only used by the very first Ruby Pact clients that supported the 'pacts for verification'
20
+ # feature, until it became clear that the parameters for the request were going to get nested and complex,
21
+ # at which point the POST was added.
18
22
  def allowed_methods
19
23
  ["GET", "POST", "OPTIONS"]
20
24
  end
@@ -32,6 +36,7 @@ module PactBroker
32
36
  end
33
37
  end
34
38
 
39
+ # For this endoint, the POST is a "read" action (used for Pactflow)
35
40
  def read_methods
36
41
  super + %w{POST}
37
42
  end
@@ -31,7 +31,7 @@ module PactBroker
31
31
  end
32
32
 
33
33
  def versions
34
- @versions ||= version_service.find_all_pacticipant_versions_in_reverse_order(pacticipant_name, pagination_options)
34
+ @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, {}, pagination_options, decorator_class(:versions_decorator).eager_load_associations)
35
35
  end
36
36
 
37
37
  def policy_name
@@ -85,6 +85,9 @@ module PactBroker
85
85
  add ["pacticipants", :pacticipant_name], Api::Resources::Pacticipant, {resource_name: "pacticipant"}
86
86
  add ["pacticipants", :pacticipant_name, "labels", :label_name], Api::Resources::Label, {resource_name: "pacticipant_label"}
87
87
 
88
+ # Labels
89
+ add ["labels"], Api::Resources::Labels, {resource_name: "labels"}
90
+
88
91
  # Versions
89
92
  add ["pacticipants", :pacticipant_name, "versions"], Api::Resources::Versions, {resource_name: "pacticipant_versions"}
90
93
  add ["pacticipants", :pacticipant_name, "branches", :branch_name, "versions"], Api::Resources::BranchVersions, {resource_name: "pacticipant_branch_versions"}
@@ -92,6 +95,7 @@ module PactBroker
92
95
  add ["pacticipants", :pacticipant_name, "latest-version", :tag], Api::Resources::LatestVersion, {resource_name: "latest_tagged_pacticipant_version"}
93
96
  add ["pacticipants", :pacticipant_name, "latest-version", :tag, "can-i-deploy", "to", :to], Api::Resources::CanIDeployPacticipantVersionByTagToTag, { resource_name: "can_i_deploy_latest_tagged_version_to_tag" }
94
97
  add ["pacticipants", :pacticipant_name, "latest-version", :tag, "can-i-deploy", "to", :to, "badge"], Api::Resources::CanIDeployPacticipantVersionByTagToTagBadge, { resource_name: "can_i_deploy_latest_tagged_version_to_tag_badge" }
98
+ add ["pacticipants", :pacticipant_name, "main-branch", "can-i-merge", "badge"], Api::Resources::CanIMergeBadge, { resource_name: "can_i_merge_badge" }
95
99
  add ["pacticipants", :pacticipant_name, "latest-version"], Api::Resources::LatestVersion, {resource_name: "latest_pacticipant_version"}
96
100
  add ["pacticipants", :pacticipant_name, "versions", :pacticipant_version_number, "tags", :tag_name], Api::Resources::Tag, {resource_name: "pacticipant_version_tag"}
97
101
  add ["pacticipants", :pacticipant_name, "branches"], Api::Resources::PacticipantBranches, {resource_name: "pacticipant_branches"}
@@ -27,6 +27,7 @@ require "pact_broker/config/basic_auth_configuration"
27
27
  require "pact_broker/api/authorization/resource_access_policy"
28
28
  require "pact_broker/api/middleware/http_debug_logs"
29
29
  require "pact_broker/application_context"
30
+ require "pact_broker/db/advisory_lock"
30
31
 
31
32
  module PactBroker
32
33
 
@@ -101,22 +102,19 @@ module PactBroker
101
102
 
102
103
  def prepare_database
103
104
  logger.info "Database schema version is #{PactBroker::DB.version(configuration.database_connection)}"
105
+ lock = PactBroker::DB::AdvisoryLock.new(configuration.database_connection, :migrate, :pg_advisory_lock)
104
106
  if configuration.auto_migrate_db
105
- migration_options = { allow_missing_migration_files: configuration.allow_missing_migration_files }
106
- if PactBroker::DB.is_current?(configuration.database_connection, migration_options)
107
- logger.info "Skipping database migrations as the latest migration has already been applied"
108
- else
109
- logger.info "Migrating database schema"
110
- PactBroker::DB.run_migrations configuration.database_connection, migration_options
111
- logger.info "Database schema version is now #{PactBroker::DB.version(configuration.database_connection)}"
107
+ lock.with_lock do
108
+ ensure_all_database_migrations_are_applied
112
109
  end
113
110
  else
114
111
  logger.info "Skipping database schema migrations as database auto migrate is disabled"
115
112
  end
116
113
 
117
114
  if configuration.auto_migrate_db_data
118
- logger.info "Migrating data"
119
- PactBroker::DB.run_data_migrations configuration.database_connection
115
+ lock.with_lock do
116
+ run_data_migrations
117
+ end
120
118
  else
121
119
  logger.info "Skipping data migrations"
122
120
  end
@@ -125,6 +123,23 @@ module PactBroker
125
123
  PactBroker::Webhooks::Service.fail_retrying_triggered_webhooks
126
124
  end
127
125
 
126
+ def ensure_all_database_migrations_are_applied
127
+ migration_options = { allow_missing_migration_files: configuration.allow_missing_migration_files }
128
+
129
+ if PactBroker::DB.is_current?(configuration.database_connection, migration_options)
130
+ logger.info "Skipping database migrations as the latest migration has already been applied"
131
+ else
132
+ logger.info "Migrating database schema"
133
+ PactBroker::DB.run_migrations(configuration.database_connection, migration_options)
134
+ logger.info "Database schema version is now #{PactBroker::DB.version(configuration.database_connection)}"
135
+ end
136
+ end
137
+
138
+ def run_data_migrations
139
+ logger.info "Migrating data"
140
+ PactBroker::DB.run_data_migrations(configuration.database_connection)
141
+ end
142
+
128
143
  def load_configuration_from_database
129
144
  configuration.load_from_database!
130
145
  end
@@ -40,6 +40,24 @@ module PactBroker
40
40
  build_shield_io_uri(title, status, color)
41
41
  end
42
42
 
43
+ def can_i_merge_badge_url(deployable: nil)
44
+ title = "can-i-merge"
45
+
46
+ # rubocop:disable Layout/EndAlignment
47
+ color, status = case deployable
48
+ when nil
49
+ [ "lightgrey", "unknown" ]
50
+ when true
51
+ [ "brightgreen", "success" ]
52
+ else
53
+ [ "red", "failed" ]
54
+ end
55
+ # rubocop:enable Layout/EndAlignment
56
+
57
+ # left text is "can-i-merge", right text is the version number
58
+ build_shield_io_uri(title, status, color)
59
+ end
60
+
43
61
  def error_badge_url(left_text, right_text)
44
62
  build_shield_io_uri(left_text, right_text, "lightgrey")
45
63
  end
@@ -35,7 +35,7 @@ module PactBroker
35
35
  version, version_notices = create_version(parsed_contracts)
36
36
  tags = create_tags(parsed_contracts, version)
37
37
  pacts, pact_notices = create_pacts(parsed_contracts, base_url)
38
- update_integrations(pacts)
38
+ create_or_update_integrations(pacts)
39
39
  notices = version_notices + pact_notices
40
40
  ContractsPublicationResults.from_hash(
41
41
  pacticipant: version.pacticipant,
@@ -304,7 +304,11 @@ module PactBroker
304
304
  PactBroker::Api::PactBrokerUrls.triggered_webhook_logs_url(triggered_webhook, base_url)
305
305
  end
306
306
 
307
- def update_integrations(pacts)
307
+ # Creating/updating the integrations all at once at the end of the transaction instead
308
+ # of one by one, as each pact is created, reduces the amount of time that
309
+ # a lock is held on the integrations table, therefore reducing contention
310
+ # and potential for deadlocks when there are many pacts being published at once.
311
+ def create_or_update_integrations(pacts)
308
312
  integration_service.handle_bulk_contract_data_published(pacts)
309
313
  end
310
314
 
@@ -0,0 +1,58 @@
1
+ require "pact_broker/logging"
2
+
3
+ # Uses a Postgres advisory lock to ensure that a given block of code can only have ONE
4
+ # thread in excution at a time against a given database.
5
+ # When the database is not Postgres, the block will yield without any locks, allowing
6
+ # this class to be used safely with other database types, but without the locking
7
+ # functionality.
8
+ #
9
+ # This is a wrapper around the actual implementation code in the Sequel extension from https://github.com/yuryroot/sequel-pg_advisory_lock
10
+ # which was copied into this codebase and modified for usage in this codebase.
11
+ #
12
+ # See https://www.postgresql.org/docs/16/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS for docs on lock types
13
+ #
14
+
15
+ module PactBroker
16
+ module DB
17
+ class AdvisoryLock
18
+ include PactBroker::Logging
19
+
20
+ def initialize(database_connection, name, type = :pg_try_advisory_lock)
21
+ @database_connection = database_connection
22
+ @name = name
23
+ @type = type
24
+ @lock_obtained = false
25
+ register_advisory_lock if postgres?
26
+ end
27
+
28
+ def with_lock
29
+ if postgres?
30
+ @database_connection.with_advisory_lock(@name) do
31
+ logger.debug("Lock #{@name} obtained")
32
+ @lock_obtained = true
33
+ yield
34
+ end
35
+ else
36
+ logger.warn("Executing block without lock as this is not a Postgres database")
37
+ @lock_obtained = true
38
+ yield
39
+ end
40
+ end
41
+
42
+ def lock_obtained?
43
+ @lock_obtained
44
+ end
45
+
46
+ private
47
+
48
+ def postgres?
49
+ @database_connection.adapter_scheme.to_s =~ /postgres/
50
+ end
51
+
52
+ def register_advisory_lock
53
+ @database_connection.extension :pg_advisory_lock
54
+ @database_connection.register_advisory_lock(@name, @type)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -4,9 +4,10 @@ require "pact_broker/versions/eager_loaders"
4
4
 
5
5
  module PactBroker
6
6
  module Domain
7
- VERSION_COLUMNS = [:id, :number, :repository_ref, :pacticipant_id, :order, :created_at, :updated_at, :build_url]
7
+ class Version < Sequel::Model
8
+ VERSION_COLUMNS = Sequel::Model.db.schema(:versions).collect(&:first) - [:branch] # do not include the branch column, as we now have a branches table
9
+ set_dataset(Sequel::Model.db[:versions].select(*VERSION_COLUMNS.collect{ | column | Sequel.qualify(:versions, column) }))
8
10
 
9
- class Version < Sequel::Model(Sequel::Model.db[:versions].select(*VERSION_COLUMNS.collect{ | column | Sequel.qualify(:versions, column) }))
10
11
  set_primary_key :id
11
12
 
12
13
  plugin :timestamps, update_on_create: true