From 3dd19955e71246c5a98317ce6453e55a22d78daa Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Mon, 7 Jun 2021 08:37:33 +1000 Subject: [PATCH] feat: add deployed version resource, supporting marking deployed version as undeployed --- lib/pact_broker/api.rb | 2 + .../decorators/deployed_version_decorator.rb | 6 ++ .../api/decorators/environment_decorator.rb | 11 ++- lib/pact_broker/api/pact_broker_urls.rb | 16 +++- ...ntly_deployed_versions_for_environment.rb} | 22 ++++- .../api/resources/deployed_version.rb | 94 +++++++++++++++++++ ...ed_versions_for_version_and_environment.rb | 6 +- .../deployments/deployed_version.rb | 7 +- .../deployments/deployed_version_service.rb | 36 +++++-- lib/pact_broker/locale/en.yml | 1 + ..._deployed_versions_for_environment_spec.rb | 41 ++++++++ spec/features/record_undeployment_spec.rb | 67 +++++++++++++ .../modifiable_resources.approved.json | 3 + .../default_base_resource_approval_spec.rb | 2 +- .../resources/default_base_resource_spec.rb | 2 +- 15 files changed, 292 insertions(+), 24 deletions(-) rename lib/pact_broker/api/resources/{deployed_versions_for_environment.rb => currently_deployed_versions_for_environment.rb} (57%) create mode 100644 lib/pact_broker/api/resources/deployed_version.rb create mode 100644 spec/features/get_currently_deployed_versions_for_environment_spec.rb create mode 100644 spec/features/record_undeployment_spec.rb diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index 195ea28dd..5bf8f3f10 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -122,9 +122,11 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_ if PactBroker.feature_enabled?(:environments) add ["environments"], Api::Resources::Environments, { resource_name: "environments" } add ["environments", :environment_uuid], Api::Resources::Environment, { resource_name: "environment" } + add ["environments", :environment_uuid, "currently-deployed-versions"], Api::Resources::CurrentlyDeployedVersionsForEnvironment, { resource_name: "environment_deployed_versions" } add ["pacticipants", :pacticipant_name, "versions", :pacticipant_version_number, "deployed-versions", "environment", :environment_uuid], Api::Resources::DeployedVersionsForVersionAndEnvironment, { resource_name: "deployed_versions_for_version_and_environment" } add ["pacticipants", :pacticipant_name, "versions", :pacticipant_version_number, "released-versions", "environment", :environment_uuid], Api::Resources::ReleasedVersionsForVersionAndEnvironment, { resource_name: "released_versions_for_version_and_environment" } add ["released-versions", :uuid], Api::Resources::ReleasedVersion, { resource_name: "released_version" } + add ["deployed-versions", :uuid], Api::Resources::DeployedVersion, { resource_name: "deployed_version" } end add ["integrations"], Api::Resources::Integrations, {resource_name: "integrations"} diff --git a/lib/pact_broker/api/decorators/deployed_version_decorator.rb b/lib/pact_broker/api/decorators/deployed_version_decorator.rb index c548cd5f1..9aaecd231 100644 --- a/lib/pact_broker/api/decorators/deployed_version_decorator.rb +++ b/lib/pact_broker/api/decorators/deployed_version_decorator.rb @@ -13,6 +13,12 @@ class DeployedVersionDecorator < BaseDecorator property :target, camelize: true include Timestamps property :undeployedAt, getter: lambda { |_| undeployed_at ? FormatDateTime.call(undeployed_at) : nil }, writeable: false + + link :self do | user_options | + { + href: deployed_version_url(represented, user_options.fetch(:base_url)) + } + end end end end diff --git a/lib/pact_broker/api/decorators/environment_decorator.rb b/lib/pact_broker/api/decorators/environment_decorator.rb index ab8ed03df..16495a1a8 100644 --- a/lib/pact_broker/api/decorators/environment_decorator.rb +++ b/lib/pact_broker/api/decorators/environment_decorator.rb @@ -17,11 +17,18 @@ class EnvironmentDecorator < BaseDecorator include Timestamps - link :self do | options | + link :self do | user_options | { title: "Environment", name: represented.name, - href: environment_url(represented, options[:base_url]) + href: environment_url(represented, user_options.fetch(:base_url)) + } + end + + link :'pb:currently-deployed-versions' do | user_options | + { + title: "Versions currently deployed to #{represented.display_name} environment", + href: currently_deployed_versions_for_environment_url(represented, user_options.fetch(:base_url)) } end diff --git a/lib/pact_broker/api/pact_broker_urls.rb b/lib/pact_broker/api/pact_broker_urls.rb index 4ed7b0059..b42104236 100644 --- a/lib/pact_broker/api/pact_broker_urls.rb +++ b/lib/pact_broker/api/pact_broker_urls.rb @@ -318,16 +318,24 @@ def deployed_versions_for_version_and_environment_url(version, environment, base "#{version_url(base_url, version)}/deployed-versions/environment/#{environment.uuid}" end + def currently_deployed_versions_for_environment_url(environment, base_url = "") + "#{base_url}/environments/#{environment.uuid}/currently-deployed-versions" + end + + def record_undeployment_url(deployed_version, base_url = "") + "#{deployed_version_url(deployed_version, base_url)}/record-undeployment" + end + def released_versions_for_version_and_environment_url(version, environment, base_url = "") "#{version_url(base_url, version)}/released-versions/environment/#{environment.uuid}" end - def deployed_version_url(deployed_version, _base_url = "") - "/deployed-versions/#{deployed_version.uuid}" + def deployed_version_url(deployed_version, base_url = "") + "#{base_url}/deployed-versions/#{deployed_version.uuid}" end - def released_version_url(released_version, _base_url = "") - "/released-versions/#{released_version.uuid}" + def released_version_url(released_version, base_url = "") + "#{base_url}/released-versions/#{released_version.uuid}" end def hal_browser_url target_url, base_url = "" diff --git a/lib/pact_broker/api/resources/deployed_versions_for_environment.rb b/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb similarity index 57% rename from lib/pact_broker/api/resources/deployed_versions_for_environment.rb rename to lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb index 28b371b9e..9742e5b3a 100644 --- a/lib/pact_broker/api/resources/deployed_versions_for_environment.rb +++ b/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb @@ -1,10 +1,13 @@ require "pact_broker/api/resources/base_resource" require "pact_broker/api/decorators/versions_decorator" +require "pact_broker/string_refinements" module PactBroker module Api module Resources - class DeployedVersionsForEnvironment < BaseResource + class CurrentlyDeployedVersionsForEnvironment < BaseResource + using PactBroker::StringRefinements + def content_types_accepted [["application/json", :from_json]] end @@ -26,7 +29,7 @@ def to_json end def policy_name - :'versions::versions' + :'deployments::environments' end private @@ -36,15 +39,26 @@ def environment end def deployed_versions - @deployed_versions ||= deployed_version_service.find_deployed_versions_for_environment(environment) + @deployed_versions ||= deployed_version_service.find_currently_deployed_versions_for_environment(environment, query_params) end def environment_uuid identifier_from_path[:environment_uuid] end + def query_params + q = {} + if request.query["pacticipant"] + q[:pacticipant_name] = request.query["pacticipant"] + end + if request.query["target"] + q[:target] = request.query["target"].blank? ? nil : request.query["target"] + end + q + end + def title - "Deployed versions for #{environment.display_name}" + "Currently deployed versions for #{environment.display_name}" end end end diff --git a/lib/pact_broker/api/resources/deployed_version.rb b/lib/pact_broker/api/resources/deployed_version.rb new file mode 100644 index 000000000..ed8ea0fb7 --- /dev/null +++ b/lib/pact_broker/api/resources/deployed_version.rb @@ -0,0 +1,94 @@ +require 'pact_broker/api/resources/base_resource' +require 'pact_broker/api/decorators/deployed_version_decorator' +require 'pact_broker/messages' + +module PactBroker + module Api + module Resources + class DeployedVersion < BaseResource + include PactBroker::Messages + + def initialize + super + @currently_deployed_param = params(default: {})[:currentlyDeployed] + end + + def content_types_provided + [ + ["application/hal+json", :to_json] + ] + end + + def content_types_accepted + [ + ["application/merge-patch+json", :from_merge_patch_json] + ] + end + + def allowed_methods + ["GET", "PATCH", "OPTIONS"] + end + + def resource_exists? + !!deployed_version + end + + def malformed_request? + if request.patch? + return invalid_json? + else + false + end + end + + def to_json + decorator_class(:deployed_version_decorator).new(deployed_version).to_json(decorator_options) + end + + def from_merge_patch_json + if request.patch? + if resource_exists? + process_currently_deployed_param + else + 404 + end + else + 415 + end + end + + def policy_name + :'versions::version' + end + + def policy_record + deployed_version&.version + end + + private + + attr_reader :currently_deployed_param + + def process_currently_deployed_param + if currently_deployed_param == false + @deployed_version = deployed_version_service.record_version_undeployed(deployed_version) + response.body = to_json + elsif currently_deployed_param == true + set_json_validation_error_messages(currentlyDeployed: [message("errors.validation.cannot_set_currently_deployed_true")]) + 422 + else + response.body = to_json + end + end + + def deployed_version + @deployed_version ||= deployed_version_service.find_by_uuid(uuid) + end + + def uuid + identifier_from_path[:uuid] + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb b/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb index ba5d4e18b..bdaa2e879 100644 --- a/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb +++ b/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb @@ -44,7 +44,11 @@ def to_json end def policy_name - :'versions::versions' + :'versions::version' + end + + def policy_record + version end private diff --git a/lib/pact_broker/deployments/deployed_version.rb b/lib/pact_broker/deployments/deployed_version.rb index 43f57d29c..bf001d6cf 100644 --- a/lib/pact_broker/deployments/deployed_version.rb +++ b/lib/pact_broker/deployments/deployed_version.rb @@ -61,7 +61,7 @@ def order_by_date_desc end def record_undeployed - update(undeployed_at: Sequel.datetime_class.now) + where(undeployed_at: nil).update(undeployed_at: Sequel.datetime_class.now) end end @@ -88,6 +88,11 @@ def currently_deployed def version_number version.number end + + def record_undeployed + self.class.where(id: id).record_undeployed + self.refresh + end end end end diff --git a/lib/pact_broker/deployments/deployed_version_service.rb b/lib/pact_broker/deployments/deployed_version_service.rb index 2fe990560..ddb4c4059 100644 --- a/lib/pact_broker/deployments/deployed_version_service.rb +++ b/lib/pact_broker/deployments/deployed_version_service.rb @@ -3,10 +3,16 @@ module PactBroker module Deployments class DeployedVersionService + def self.next_uuid SecureRandom.uuid end + # Policy applied at resource level to Version + def self.find_by_uuid(uuid) + DeployedVersion.where(uuid: uuid).single_record + end + def self.create(uuid, version, environment, target) record_previous_version_undeployed(version.pacticipant, environment, target) DeployedVersion.create( @@ -19,12 +25,13 @@ def self.create(uuid, version, environment, target) end def self.find_deployed_versions_for_version_and_environment(version, environment) - DeployedVersion + scope_for(DeployedVersion) .for_version_and_environment(version, environment) .order_by_date_desc .all end + # Policy applied at resource level to Version def self.find_currently_deployed_version_for_version_and_environment_and_target(version, environment, target) DeployedVersion .currently_deployed @@ -32,15 +39,19 @@ def self.find_currently_deployed_version_for_version_and_environment_and_target( .single_record end - def self.find_deployed_versions_for_environment(environment) - DeployedVersion + def self.find_currently_deployed_versions_for_environment(environment, pacticipant_name: nil, target: nil) + query = scope_for(DeployedVersion) + .currently_deployed .for_environment(environment) .order_by_date_desc - .all + + query = query.for_pacticipant_name(pacticipant_name) if pacticipant_name + query = query.for_target(target) if target + query.all end def self.find_currently_deployed_versions_for_pacticipant(pacticipant) - DeployedVersion + scope_for(DeployedVersion) .currently_deployed .where(pacticipant_id: pacticipant.id) .eager(:version) @@ -49,13 +60,10 @@ def self.find_currently_deployed_versions_for_pacticipant(pacticipant) end def self.record_version_undeployed(deployed_version) - deployed_version.currently_deployed_version_id.delete - # CurrentlyDeployedVersionId.where(pacticipant_id: pacticipant.id, environment_id: environment.id, target: target).delete - record_previous_version_undeployed(deployed_version.version.pacticipant, deployed_version.environment, deployed_version.target) + deployed_version.currently_deployed_version_id&.delete + deployed_version.record_undeployed end - # private - def self.record_previous_version_undeployed(pacticipant, environment, target) DeployedVersion.where( undeployed_at: nil, @@ -64,6 +72,14 @@ def self.record_previous_version_undeployed(pacticipant, environment, target) target: target ).record_undeployed end + + private_class_method :record_previous_version_undeployed + + def self.scope_for(scope) + PactBroker.policy_scope!(scope) + end + + private_class_method :scope_for end end end diff --git a/lib/pact_broker/locale/en.yml b/lib/pact_broker/locale/en.yml index e17caf922..6d5417f93 100644 --- a/lib/pact_broker/locale/en.yml +++ b/lib/pact_broker/locale/en.yml @@ -81,6 +81,7 @@ en: environment_with_name_not_found: "Environment with name '%{name}' does not exist" cannot_modify_version_branch: "The branch for a pacticipant version cannot be changed once set (currently '%{old_branch}', proposed value '%{new_branch}'). Your pact publication/verification publication configurations may be in conflict with each other if you are seeing this error. If the current branch value is incorrect, you must delete the pacticipant version resource at %{version_url} and publish the pacts/verification results again with a consistent branch name." invalid_content_for_content_type: "The content could not be parsed as %{content_type}" + cannot_set_currently_deployed_true: The currentlyDeployed property cannot be set back to true. Please record a new deployment. duplicate_pacticipant: | This is the first time a pact has been published for "%{new_name}". The name "%{new_name}" is very similar to the following existing consumers/providers: diff --git a/spec/features/get_currently_deployed_versions_for_environment_spec.rb b/spec/features/get_currently_deployed_versions_for_environment_spec.rb new file mode 100644 index 000000000..23add85e2 --- /dev/null +++ b/spec/features/get_currently_deployed_versions_for_environment_spec.rb @@ -0,0 +1,41 @@ +RSpec.describe "Get currently deployed versions for environment" do + let!(:version) { td.create_consumer("Foo").create_consumer_version("1").and_return(:consumer_version) } + let!(:test_environment) { td.create_environment("test").and_return(:environment) } + let!(:prod_environment) { td.create_environment("prod").and_return(:environment) } + let!(:deployed_version) do + td.create_deployed_version_for_consumer_version(environment_name: "test", target: "customer-1", created_at: DateTime.now - 2) + .create_deployed_version_for_consumer_version(environment_name: "prod", created_at: DateTime.now - 1) + .create_provider("Bar") + .create_provider_version("4") + .create_deployed_version_for_provider_version(environment_name: "test", target: "customer-1") + .create_provider_version("5") + .create_deployed_version_for_provider_version(environment_name: "test", target: "customer-2") + + end + + let(:path) do + PactBroker::Api::PactBrokerUrls.currently_deployed_versions_for_environment_url( + test_environment + ) + end + + let(:response_body_hash) { JSON.parse(subject.body, symbolize_names: true) } + + subject { get(path, nil, { "HTTP_ACCEPT" => "application/hal+json" }) } + + it "returns a list of deployed versions" do + expect(response_body_hash[:_embedded][:deployedVersions]).to be_a(Array) + expect(response_body_hash[:_embedded][:deployedVersions].size).to eq 3 + expect(response_body_hash[:_links][:self][:title]).to eq "Currently deployed versions for Test" + expect(response_body_hash[:_links][:self][:href]).to end_with(path) + end + + context "with query params" do + subject { get(path, { pacticipant: "Bar", target: "customer-1" }, { "HTTP_ACCEPT" => "application/hal+json" }) } + + it "returns a list of matching deployed versions" do + expect(response_body_hash[:_embedded][:deployedVersions].size).to eq 1 + expect(response_body_hash[:_embedded][:deployedVersions].first[:_embedded][:version][:number]).to eq "4" + end + end +end diff --git a/spec/features/record_undeployment_spec.rb b/spec/features/record_undeployment_spec.rb new file mode 100644 index 000000000..82f133917 --- /dev/null +++ b/spec/features/record_undeployment_spec.rb @@ -0,0 +1,67 @@ +RSpec.describe "Get currently deployed versions for environment" do + let!(:version) { td.create_consumer("Foo").create_consumer_version("1").and_return(:consumer_version) } + let!(:test_environment) { td.create_environment("test").and_return(:environment) } + let!(:deployed_version) do + td.create_deployed_version_for_consumer_version(environment_name: "test", target: "customer-1", created_at: DateTime.now - 2, currently_deployed: currently_deployed) + .and_return(:deployed_version) + end + let(:currently_deployed) { true } + let(:path) { PactBroker::Api::PactBrokerUrls.deployed_version_url(deployed_version) } + let(:request_body) { { currentlyDeployed: false }.to_json } + let(:response_body_hash) { JSON.parse(subject.body) } + let(:rack_headers) do + { "HTTP_ACCEPT" => "application/hal+json", "CONTENT_TYPE" => "application/merge-patch+json" } + end + + subject { patch(path, request_body, rack_headers) } + + it "marks the deployed version as not currently deployed" do + expect{ subject }.to change { + PactBroker::Deployments::DeployedVersion.find(uuid: deployed_version.uuid).currently_deployed + }.from(true).to(false) + end + + it "returns the updated resource" do + expect(response_body_hash["currentlyDeployed"]).to be false + expect(response_body_hash["undeployedAt"]).to_not be nil + end + + context "with an empty body" do + let(:request_body) { {}.to_json } + + it "does nothing to the resource" do + expect{ subject }.to_not change { + PactBroker::Deployments::DeployedVersion.find(uuid: deployed_version.uuid).values + } + end + + it "returns the resource" do + expect(response_body_hash["uuid"]).to eq deployed_version.uuid + end + end + + context "when the version is already undeployed" do + let(:currently_deployed) { false } + + it "returns the resource" do + expect(response_body_hash["currentlyDeployed"]).to be false + expect(response_body_hash["undeployedAt"]).to_not be nil + end + + it "does not change the undeployedAt date" do + expect{ subject }.to_not change { + PactBroker::Deployments::DeployedVersion.find(uuid: deployed_version.uuid).undeployed_at + } + end + + context "when trying to mark it as currentlyDeployed again" do + let(:request_body) { { currentlyDeployed: true }.to_json } + + its(:status) { is_expected.to eq 422 } + + it "returns an error" do + expect(response_body_hash["errors"]["currentlyDeployed"].first).to include "cannot be set back" + end + end + end +end diff --git a/spec/fixtures/approvals/modifiable_resources.approved.json b/spec/fixtures/approvals/modifiable_resources.approved.json index 90fe477ad..29fd60adc 100644 --- a/spec/fixtures/approvals/modifiable_resources.approved.json +++ b/spec/fixtures/approvals/modifiable_resources.approved.json @@ -6,6 +6,9 @@ { "resource_class_name": "PactBroker::Api::Resources::Clean" }, + { + "resource_class_name": "PactBroker::Api::Resources::DeployedVersion" + }, { "resource_class_name": "PactBroker::Api::Resources::DeployedVersionsForVersionAndEnvironment" }, diff --git a/spec/lib/pact_broker/api/resources/default_base_resource_approval_spec.rb b/spec/lib/pact_broker/api/resources/default_base_resource_approval_spec.rb index d7e3bddcc..71bde3fab 100644 --- a/spec/lib/pact_broker/api/resources/default_base_resource_approval_spec.rb +++ b/spec/lib/pact_broker/api/resources/default_base_resource_approval_spec.rb @@ -15,7 +15,7 @@ module Resources data = pact_broker_resource_classes.collect do | resource_class | application_context = PactBroker::ApplicationContext.default_application_context path_info = { pacticipant_name: "Foo", pacticipant_version_number: "1", application_context: application_context } - request = double("request", uri: URI("http://example.org"), path_info: path_info).as_null_object + request = double("request", uri: URI("http://example.org"), path_info: path_info, body: "{}").as_null_object response = double("response") resource = resource_class.new(request, response) modifiable = resource.allowed_methods.any?{ | method | %w{PATCH POST PUT DELETE}.include?(method) } diff --git a/spec/lib/pact_broker/api/resources/default_base_resource_spec.rb b/spec/lib/pact_broker/api/resources/default_base_resource_spec.rb index ad8eb130c..0574dba57 100644 --- a/spec/lib/pact_broker/api/resources/default_base_resource_spec.rb +++ b/spec/lib/pact_broker/api/resources/default_base_resource_spec.rb @@ -191,7 +191,7 @@ module Resources allow(path_info).to receive(:[]).with(:application_context).and_return(application_context) end let(:application_context) { PactBroker::ApplicationContext.default_application_context(before_resource: before_resource, after_resource: after_resource) } - let(:request) { double("request", uri: URI("http://example.org"), path_info: path_info).as_null_object } + let(:request) { double("request", uri: URI("http://example.org"), path_info: path_info, body: "{}").as_null_object } let(:path_info) { { pacticipant_name: "foo", pacticipant_version_number: "1" } } let(:response) { double("response").as_null_object } let(:resource) { resource_class.new(request, response) }