diff --git a/app/events/merge_requests/closed_event.rb b/app/events/merge_requests/closed_event.rb new file mode 100644 index 0000000000000000000000000000000000000000..dff184d23056a515c517f550fd81b6d0069af80e --- /dev/null +++ b/app/events/merge_requests/closed_event.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module MergeRequests + class ClosedEvent < Gitlab::EventStore::Event + def schema + { + 'type' => 'object', + 'required' => %w[ + current_user_id + merge_request_id + ], + 'properties' => { + 'current_user_id' => { 'type' => 'integer' }, + 'merge_request_id' => { 'type' => 'integer' } + } + } + end + end +end diff --git a/app/events/merge_requests/merged_event.rb b/app/events/merge_requests/merged_event.rb new file mode 100644 index 0000000000000000000000000000000000000000..8edc1a1c005499f6979f5339719f77f383ca1a66 --- /dev/null +++ b/app/events/merge_requests/merged_event.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module MergeRequests + class MergedEvent < Gitlab::EventStore::Event + def schema + { + 'type' => 'object', + 'required' => %w[ + current_user_id + merge_request_id + ], + 'properties' => { + 'current_user_id' => { 'type' => 'integer' }, + 'merge_request_id' => { 'type' => 'integer' } + } + } + end + end +end diff --git a/app/events/pages/merge_request_preview_outdated_event.rb b/app/events/pages/merge_request_preview_outdated_event.rb new file mode 100644 index 0000000000000000000000000000000000000000..16d13a6d3774a71cfec685e312b0616df20bd505 --- /dev/null +++ b/app/events/pages/merge_request_preview_outdated_event.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Pages + class MergeRequestPreviewOutdatedEvent < Gitlab::EventStore::Event + def schema + { + 'type' => 'object', + 'required' => %w[ + pages_deployment_id + ], + 'properties' => { + 'pages_deployment_id' => { 'type' => 'integer' } + } + } + end + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index bb1bfe8c889298d1906ad2dcbe9af53a1a39871e..d82a1d0bd470fe29ec4fdbaf2ffbfcca297fc794 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -328,7 +328,8 @@ def clone_accessors after_transition any => [:success] do |build| build.run_after_commit do BuildSuccessWorker.perform_async(id) - PagesWorker.perform_async(:deploy, id) if build.pages_generator? + ::PagesWorker.perform_async(:deploy, id) if build.pages_generator? + ::Pages::CreateMergeRequestPreviewWorker.perform_async(id) if build.pages_merge_request_preview_generator? end end @@ -413,6 +414,16 @@ def pages_generator? name == 'pages' end + def pages_merge_request_preview_generator? + merge_request.present? && + Gitlab.config.pages.enabled && + name == Pages::MergeRequestPreview::CI_JOB_NAME + end + + def pages_merge_request_preview_url + (merge_request.pages_preview || merge_request.create_pages_preview).url + end + def runnable? true end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 116108ceaf95953210bbea806ffaec5ba22f4bfc..48f44eb1d960285472473f5930b0113b5b89cb85 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -120,6 +120,10 @@ def merge_request_diff has_many :created_environments, class_name: 'Environment', foreign_key: :merge_request_id, inverse_of: :merge_request has_many :assignment_events, class_name: 'ResourceEvents::MergeRequestAssignmentEvent', inverse_of: :merge_request + has_one :pages_preview, + class_name: 'Pages::MergeRequestPreview', + inverse_of: :merge_request + KNOWN_MERGE_PARAMS = [ :auto_merge_strategy, :should_remove_source_branch, diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index 864ea04c0190313374d52a8efe9beaaa95d7dc78..df65f8717294fd5455eb4725e2ea33c977937430 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -6,10 +6,11 @@ class LookupPath LegacyStorageDisabledError = Class.new(::StandardError) - def initialize(project, trim_prefix: nil, domain: nil) + def initialize(project, trim_prefix: nil, domain: nil, deployment: nil) @project = project @domain = domain @trim_prefix = trim_prefix || project.full_path + @deployment = deployment || project.pages_metadatum.pages_deployment end def project_id @@ -70,11 +71,6 @@ def root_directory private - attr_reader :project, :trim_prefix, :domain - - def deployment - project.pages_metadatum.pages_deployment - end - strong_memoize_attr :deployment + attr_reader :project, :trim_prefix, :domain, :deployment end end diff --git a/app/models/pages/merge_request_preview.rb b/app/models/pages/merge_request_preview.rb new file mode 100644 index 0000000000000000000000000000000000000000..a298e473fe1ddf8c60a505007f85f6d68fdac98a --- /dev/null +++ b/app/models/pages/merge_request_preview.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Pages + class MergeRequestPreview < ::ApplicationRecord + SLUG_PREFIX = 'preview' + CI_JOB_NAME = 'pages:preview' + + self.table_name = 'pages_merge_request_previews' + + belongs_to :merge_request, + class_name: 'MergeRequest', + inverse_of: :pages_preview, + optional: false + + belongs_to :deployment, + class_name: 'PagesDeployment', + foreign_key: :pages_deployment_id, + inverse_of: :preview, + optional: true + + has_one :project, + through: :merge_request, + source: :target_project + + before_validation :generate_slug, unless: :slug + + validates :merge_request_id, uniqueness: true + validates :slug, presence: true, uniqueness: true + + scope :with_deployment, -> { where.not(pages_deployment_id: nil) } + scope :find_by_slug, ->(value) { find_by(slug: value) } + + def url + project&.pages_url_for(slug) + end + + private + + def generate_slug + self.slug = format( + '%s*%s*%s', + SLUG_PREFIX, + project.path.slice(0, 23), + SecureRandom.hex(30) + )[0..62] + end + end +end diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb index fafbe449c8c9827523e354811ba0611e8a5c05f6..69a3e56419f12ee50e0404da7eb92d254c511220 100644 --- a/app/models/pages/virtual_domain.rb +++ b/app/models/pages/virtual_domain.rb @@ -2,11 +2,12 @@ module Pages class VirtualDomain - def initialize(projects:, cache: nil, trim_prefix: nil, domain: nil) + def initialize(projects:, cache: nil, trim_prefix: nil, domain: nil, lookup_paths: nil) @projects = projects @cache = cache @trim_prefix = trim_prefix @domain = domain + @lookup_paths = lookup_paths end def certificate @@ -18,14 +19,10 @@ def key end def lookup_paths - paths = projects.map do |project| - project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain) - end - - # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/328715 - paths = paths.select(&:source) - - paths.sort_by(&:prefix).reverse + build_lookup_paths + .select(&:source) # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/328715 + .sort_by(&:prefix) + .reverse end # cache_key is required by #present_cached in ::API::Internal::Pages @@ -36,5 +33,11 @@ def cache_key private attr_reader :projects, :trim_prefix, :domain, :cache + + def build_lookup_paths + @lookup_paths || projects.map do |project| + Pages::LookupPath.new(project, trim_prefix: trim_prefix, domain: domain) + end + end end end diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb index fa29cbf835236c65a6ed5b9f199334acf1e72b3c..8101349ad1d0734d5f0f72316e4b801c120657c0 100644 --- a/app/models/pages_deployment.rb +++ b/app/models/pages_deployment.rb @@ -8,17 +8,41 @@ class PagesDeployment < ApplicationRecord MIGRATED_FILE_NAME = "_migrated.zip" + # old deployment can be cached by pages daemon + # so we need to give pages daemon some time update cache + # 10 minutes is enough, but 30 feels safer + OLD_DEPLOYMENTS_DESTRUCTION_DELAY = 30.minutes.freeze + attribute :file_store, :integer, default: -> { ::Pages::DeploymentUploader.default_store } belongs_to :project, optional: false belongs_to :ci_build, class_name: 'Ci::Build', optional: true - scope :older_than, -> (id) { where('id < ?', id) } + has_one :preview, + class_name: 'Pages::MergeRequestPreview', + inverse_of: :deployment + + scope :find_by_id, ->(id) { find_by(id: id) } + scope :older_than, ->(id) { where('id < ?', id) } scope :migrated_from_legacy_storage, -> { where(file: MIGRATED_FILE_NAME) } scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) } scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) } scope :project_id_in, ->(ids) { where(project_id: ids) } + scope :for_merge_request_preview, ->(merge_request_id) do + joins(:preview) + .where(pages_merge_request_previews: { merge_request_id: merge_request_id }) + end + + scope :without_merge_request_preview, -> do + where( + 'NOT EXISTS (?)', + Pages::MergeRequestPreview + .select(1) + .where('pages_merge_request_previews.pages_deployment_id = pages_deployments.id') + ) + end + validates :file, presence: true validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES } validates :size, presence: true, numericality: { greater_than: 0, only_integer: true } diff --git a/app/models/project.rb b/app/models/project.rb index 452a5c8973c3952435b4c973fa4821b98db80fa7..9c89fdfce67873f2c461c0bad456f885b4ce2a2d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -3241,13 +3241,6 @@ def frozen_outbound_job_token_scopes? end strong_memoize_attr :frozen_outbound_job_token_scopes? - private - - def pages_unique_domain_enabled? - Feature.enabled?(:pages_unique_domain, self) && - project_setting.pages_unique_domain_enabled? - end - def pages_url_for(domain) # The host in URL always needs to be downcased Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix| @@ -3255,6 +3248,13 @@ def pages_url_for(domain) end.downcase end + private + + def pages_unique_domain_enabled? + Feature.enabled?(:pages_unique_domain, self) && + project_setting.pages_unique_domain_enabled? + end + # overridden in EE def project_group_links_with_preload project_group_links diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index 62928e05a8995b018493c07eedcc444c7501ea90..0ca90d19b0abfe4d665d757223bd5a51341a9677 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -25,6 +25,7 @@ def execute(merge_request, commit = nil) abort_auto_merge(merge_request, 'merge request was closed') cleanup_refs(merge_request) trigger_merge_request_merge_status_updated(merge_request) + publish_event(merge_request) end merge_request @@ -48,6 +49,15 @@ def expire_unapproved_key(merge_request) def trigger_merge_request_merge_status_updated(merge_request) GraphqlTriggers.merge_request_merge_status_updated(merge_request) end + + def publish_event(merge_request) + event = MergeRequests::ClosedEvent.new(data: { + current_user_id: current_user.id, + merge_request_id: merge_request.id + }) + + Gitlab::EventStore.publish(event) + end end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 5e41375e7a09ebafb5890abec98cfca8069e52af..e18270167f78a8a3cfcd04cd291a6f46e2db2569 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -34,7 +34,7 @@ def execute(merge_request, options = {}) if commit after_merge clean_merge_jid - success + publish_event(merge_request) end end @@ -203,5 +203,14 @@ def exclusive_lease(merge_request_id) Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) end end + + def publish_event(merge_request) + event = MergeRequests::MergedEvent.new(data: { + current_user_id: current_user.id, + merge_request_id: merge_request.id + }) + + Gitlab::EventStore.publish(event) + end end end diff --git a/app/services/pages/destroy_deployments_service.rb b/app/services/pages/destroy_deployments_service.rb index 45d906bec7ac100965ef60f1b99488e1be030ab3..fea2a1eda0217adb111344d7140be8dcc69be1d6 100644 --- a/app/services/pages/destroy_deployments_service.rb +++ b/app/services/pages/destroy_deployments_service.rb @@ -2,14 +2,17 @@ module Pages class DestroyDeploymentsService - def initialize(project, last_deployment_id = nil) + def initialize(project, last_deployment_id = nil, skip_merge_request_previews: false) @project = project @last_deployment_id = last_deployment_id + @skip_merge_request_previews = skip_merge_request_previews end def execute deployments_to_destroy = @project.pages_deployments + deployments_to_destroy = deployments_to_destroy.without_merge_request_preview if @skip_merge_request_previews deployments_to_destroy = deployments_to_destroy.older_than(@last_deployment_id) if @last_deployment_id + deployments_to_destroy.find_each(&:destroy) # rubocop: disable CodeReuse/ActiveRecord end end diff --git a/app/services/pages/update_merge_request_preview_service.rb b/app/services/pages/update_merge_request_preview_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..663d581c6624b064335307e314c88d290b56d8bf --- /dev/null +++ b/app/services/pages/update_merge_request_preview_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Pages + class UpdateMergeRequestPreviewService + def initialize(pages_deployment:, merge_request:) + @pages_deployment = pages_deployment + @merge_request = merge_request + end + + def execute + publish_outdated_preview_event(merge_request_preview.pages_deployment_id) + + merge_request_preview.deployment = pages_deployment + merge_request_preview.save! + end + + private + + attr_reader :pages_deployment, :merge_request + + def merge_request_preview + merge_request.pages_preview || merge_request.build_pages_preview + end + + def publish_outdated_preview_event(deployment_id) + return unless deployment_id.present? + + event = ::Pages::MergeRequestPreviewOutdatedEvent.new(data: { + pages_deployment_id: deployment_id + }) + + Gitlab::EventStore.publish(event) + end + end +end diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 403f645392c237c7b2020c64e3d6cda3768ff0ad..ef84d0f66d6c7fb2741c258cb02bc98f79a60ae2 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -2,17 +2,15 @@ module Projects class UpdatePagesService < BaseService - # old deployment can be cached by pages daemon - # so we need to give pages daemon some time update cache - # 10 minutes is enough, but 30 feels safer - OLD_DEPLOYMENTS_DESTRUCTION_DELAY = 30.minutes.freeze + include Gitlab::Utils::StrongMemoize attr_reader :build, :deployment_update - def initialize(project, build) + def initialize(project, build, merge_request_preview: false) @project = project @build = build @deployment_update = ::Gitlab::Pages::DeploymentUpdate.new(project, build) + @merge_request_preview = merge_request_preview end def execute @@ -31,13 +29,18 @@ def execute deployment = create_pages_deployment(artifacts_path, build) break error('The uploaded artifact size does not match the expected value') unless deployment - - if deployment_update.valid? - update_project_pages_deployment(deployment) - success - else - error(deployment_update.errors.first.full_message) - end + break error(deployment_update.errors.first.full_message) unless deployment_update.valid? + + # TODO:: to avoid "overwriting" the production pages deploy + # we only create the review app but do not update the pages deployment id in + # the project_pages_metadatum + # + # On the final version, the creation of the review app deploy will be a + # manual process by the user, which we can probably extract from here. + break update_merge_request_preview(deployment) if @merge_request_preview + + update_project_pages_deployment(deployment) + success end rescue StandardError => e error(e.message) @@ -102,21 +105,14 @@ def create_pages_deployment(artifacts_path, build) def update_project_pages_deployment(deployment) project.update_pages_deployment!(deployment) - DestroyPagesDeploymentsWorker.perform_in( - OLD_DEPLOYMENTS_DESTRUCTION_DELAY, + ::DestroyPagesDeploymentsWorker.perform_in( + ::PagesDeployment::OLD_DEPLOYMENTS_DESTRUCTION_DELAY, project.id, - deployment.id + deployment.id, + true ) end - def ref - build.ref - end - - def artifacts - build.artifacts_file.path - end - def register_attempt pages_deployments_total_counter.increment end @@ -126,12 +122,14 @@ def register_failure end def pages_deployments_total_counter - @pages_deployments_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_total, "Counter of GitLab Pages deployments triggered") + Gitlab::Metrics.counter(:pages_deployments_total, "Counter of GitLab Pages deployments triggered") end + strong_memoize_attr :pages_deployments_total_counter def pages_deployments_failed_total_counter - @pages_deployments_failed_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed") + Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed") end + strong_memoize_attr :pages_deployments_failed_total_counter def publish_deployed_event event = ::Pages::PageDeployedEvent.new(data: { @@ -142,5 +140,17 @@ def publish_deployed_event Gitlab::EventStore.publish(event) end + + def update_merge_request_preview(deployment) + # Mark pages:deploy as successful + @commit_status.success + + return if build.merge_request.blank? + + ::Pages::UpdateMergeRequestPreviewService.new( + pages_deployment: deployment, + merge_request: build.merge_request + ).execute + end end end diff --git a/app/workers/destroy_pages_deployments_worker.rb b/app/workers/destroy_pages_deployments_worker.rb index 7fa73648dd23231956ef79b9523242a6ed128705..28ec3a1000f356bcec443494872b4b4561159a0d 100644 --- a/app/workers/destroy_pages_deployments_worker.rb +++ b/app/workers/destroy_pages_deployments_worker.rb @@ -7,15 +7,19 @@ class DestroyPagesDeploymentsWorker idempotent! - loggable_arguments 0, 1 + loggable_arguments 0, 1, 2 sidekiq_options retry: 3 feature_category :pages - def perform(project_id, last_deployment_id = nil) + def perform(project_id, last_deployment_id = nil, skip_merge_request_previews = false) project = Project.find_by_id(project_id) return unless project - ::Pages::DestroyDeploymentsService.new(project, last_deployment_id).execute + ::Pages::DestroyDeploymentsService.new( + project, + last_deployment_id, + skip_merge_request_previews: skip_merge_request_previews + ).execute end end diff --git a/app/workers/pages/clear_merge_request_previews_worker.rb b/app/workers/pages/clear_merge_request_previews_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..a240eca7dc35bdcbd8aa9a6842ba13b08779e40c --- /dev/null +++ b/app/workers/pages/clear_merge_request_previews_worker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Pages + class ClearMergeRequestPreviewsWorker + include ApplicationWorker + + data_consistency :always + + sidekiq_options retry: 3 + feature_category :pages + worker_resource_boundary :cpu + + def perform(event) + PagesDeployment + .for_merge_request_preview(event['merge_request_id']) + .delete_all! + end + end +end diff --git a/app/workers/pages/create_merge_request_preview_worker.rb b/app/workers/pages/create_merge_request_preview_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..823a3efcf7ac51bb1a9e06d31cea802ca7dd4f6c --- /dev/null +++ b/app/workers/pages/create_merge_request_preview_worker.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Pages + class CreateMergeRequestPreviewWorker + include ApplicationWorker + + idempotent! + + data_consistency :always # rubocop: disable SidekiqLoadBalancing/WorkerDataConsistency + + loggable_arguments 0 + sidekiq_options retry: 3 + feature_category :pages + worker_resource_boundary :cpu + + def perform(build_id) + build = Ci::Build.find_by_id(build_id) + + return if build&.merge_request.blank? + + Projects::UpdatePagesService.new( + build.project, + build, + merge_request_preview: true + ).execute + end + end +end diff --git a/app/workers/pages/delete_merge_request_preview_worker.rb b/app/workers/pages/delete_merge_request_preview_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..703c011e9c4d9e9e6e9ef27aa0ef1a4885557191 --- /dev/null +++ b/app/workers/pages/delete_merge_request_preview_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Pages + class DeleteMergeRequestPreviewWorker + include Gitlab::EventStore::Subscriber + + idempotent! + + feature_category :pages + + def handle_event(event) + if event.data[:pages_deployment_id].present? + ::PagesDeployment + .find_by_id(event.data[:pages_deployment_id]) + .delete + elsif event.data[:merge_request_id].present? + ::PagesDeployment + .for_merge_request_preview(event.data[:merge_request_id]) + .delete_all + end + end + end +end diff --git a/db/docs/pages_merge_request_review_apps.yml b/db/docs/pages_merge_request_review_apps.yml new file mode 100644 index 0000000000000000000000000000000000000000..5785f3fc1b0802b3fe92f43064ae20fde32da4f1 --- /dev/null +++ b/db/docs/pages_merge_request_review_apps.yml @@ -0,0 +1,10 @@ +--- +table_name: pages_merge_request_previews +classes: +- Pages::MergeRequestPreviews +feature_categories: +- pages +description: Associates a review app slug with project, merge request and build +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122498 +milestone: '16.1' +gitlab_schema: gitlab_main diff --git a/db/migrate/20230610104800_create_pages_merge_request_previews.rb b/db/migrate/20230610104800_create_pages_merge_request_previews.rb new file mode 100644 index 0000000000000000000000000000000000000000..b4d126fc0bfa405d902aa52e71e8d6f1132218f1 --- /dev/null +++ b/db/migrate/20230610104800_create_pages_merge_request_previews.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class CreatePagesMergeRequestPreviews < Gitlab::Database::Migration[2.1] + def up + create_table(:pages_merge_request_previews) do |t| + t.datetime_with_timezone :created_at, null: false + t.datetime_with_timezone :updated_at, null: false + t.bigint :pages_deployment_id + t.bigint :merge_request_id, null: false + t.text :slug, null: false, limit: 63, unique: true + + t.index :pages_deployment_id + t.index :merge_request_id, unique: true + end + end + + def down + drop_table :pages_merge_request_previews + end +end diff --git a/db/migrate/20230610104801_add_pages_deployment_foreign_key_to_pages_merge_request_previews.rb b/db/migrate/20230610104801_add_pages_deployment_foreign_key_to_pages_merge_request_previews.rb new file mode 100644 index 0000000000000000000000000000000000000000..3fb66151796dbd25cdec016181e7d380590ed682 --- /dev/null +++ b/db/migrate/20230610104801_add_pages_deployment_foreign_key_to_pages_merge_request_previews.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddPagesDeploymentForeignKeyToPagesMergeRequestPreviews < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :pages_merge_request_previews, + :pages_deployments, + column: :pages_deployment_id, + on_delete: :nullify + end + + def down + with_lock_retries do + remove_foreign_key :pages_merge_request_previews, + column: :pages_deployment_id + end + end +end diff --git a/db/migrate/20230610104802_add_merge_request_foreign_key_to_pages_merge_request_previews.rb b/db/migrate/20230610104802_add_merge_request_foreign_key_to_pages_merge_request_previews.rb new file mode 100644 index 0000000000000000000000000000000000000000..f7bfcb37e895cc23c689b7c48e33f866c939f6ff --- /dev/null +++ b/db/migrate/20230610104802_add_merge_request_foreign_key_to_pages_merge_request_previews.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddMergeRequestForeignKeyToPagesMergeRequestPreviews < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :pages_merge_request_previews, + :merge_requests, + column: :merge_request_id, + on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :pages_merge_request_previews, + column: :merge_request_id + end + end +end diff --git a/db/schema_migrations/20230610104800 b/db/schema_migrations/20230610104800 new file mode 100644 index 0000000000000000000000000000000000000000..ccb865b59c86fa913d488c848edf79091debebe0 --- /dev/null +++ b/db/schema_migrations/20230610104800 @@ -0,0 +1 @@ +ea93f93c2006fbff8f8944f70eaf676e51e5d13eaa670fc72376cd3d6b7f0282 \ No newline at end of file diff --git a/db/schema_migrations/20230610104801 b/db/schema_migrations/20230610104801 new file mode 100644 index 0000000000000000000000000000000000000000..5316ea72b7316a6299a295c150b15b10d4145b44 --- /dev/null +++ b/db/schema_migrations/20230610104801 @@ -0,0 +1 @@ +c9f46a0dbd965909327b249420b959e206a8c2e6327eff324593f190a7c3c963 \ No newline at end of file diff --git a/db/schema_migrations/20230610104802 b/db/schema_migrations/20230610104802 new file mode 100644 index 0000000000000000000000000000000000000000..0dac0630199035c431aae78831567e9733ba1264 --- /dev/null +++ b/db/schema_migrations/20230610104802 @@ -0,0 +1 @@ +97b185b854b727aeca3390cf9925174ef4b232fa40c7000787c9d99701613080 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 77b32db0edac9ee5e646b3e61642dfe28bebb72b..20ffe520d75d642d33e3ae16e8aaa742f2c527b2 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -20022,6 +20022,25 @@ CREATE SEQUENCE pages_domains_id_seq ALTER SEQUENCE pages_domains_id_seq OWNED BY pages_domains.id; +CREATE TABLE pages_merge_request_previews ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + pages_deployment_id bigint, + merge_request_id bigint NOT NULL, + slug text NOT NULL, + CONSTRAINT check_5ed1917d35 CHECK ((char_length(slug) <= 63)) +); + +CREATE SEQUENCE pages_merge_request_previews_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE pages_merge_request_previews_id_seq OWNED BY pages_merge_request_previews.id; + CREATE TABLE path_locks ( id integer NOT NULL, path character varying NOT NULL, @@ -25614,6 +25633,8 @@ ALTER TABLE ONLY pages_domain_acme_orders ALTER COLUMN id SET DEFAULT nextval('p ALTER TABLE ONLY pages_domains ALTER COLUMN id SET DEFAULT nextval('pages_domains_id_seq'::regclass); +ALTER TABLE ONLY pages_merge_request_previews ALTER COLUMN id SET DEFAULT nextval('pages_merge_request_previews_id_seq'::regclass); + ALTER TABLE ONLY path_locks ALTER COLUMN id SET DEFAULT nextval('path_locks_id_seq'::regclass); ALTER TABLE ONLY personal_access_tokens ALTER COLUMN id SET DEFAULT nextval('personal_access_tokens_id_seq'::regclass); @@ -27932,6 +27953,9 @@ ALTER TABLE ONLY pages_domain_acme_orders ALTER TABLE ONLY pages_domains ADD CONSTRAINT pages_domains_pkey PRIMARY KEY (id); +ALTER TABLE ONLY pages_merge_request_previews + ADD CONSTRAINT pages_merge_request_previews_pkey PRIMARY KEY (id); + ALTER TABLE ONLY path_locks ADD CONSTRAINT path_locks_pkey PRIMARY KEY (id); @@ -32212,6 +32236,10 @@ CREATE INDEX index_pages_domains_on_verified_at_and_enabled_until ON pages_domai CREATE INDEX index_pages_domains_on_wildcard ON pages_domains USING btree (wildcard); +CREATE UNIQUE INDEX index_pages_merge_request_previews_on_merge_request_id ON pages_merge_request_previews USING btree (merge_request_id); + +CREATE INDEX index_pages_merge_request_previews_on_pages_deployment_id ON pages_merge_request_previews USING btree (pages_deployment_id); + CREATE INDEX p_ci_builds_user_id_name_idx ON ONLY p_ci_builds USING btree (user_id, name) WHERE (((type)::text = 'Ci::Build'::text) AND ((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('license_scanning'::character varying)::text, ('sast'::character varying)::text, ('coverage_fuzzing'::character varying)::text, ('secret_detection'::character varying)::text]))); CREATE INDEX index_partial_ci_builds_on_user_id_name_parser_features ON ci_builds USING btree (user_id, name) WHERE (((type)::text = 'Ci::Build'::text) AND ((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('license_scanning'::character varying)::text, ('sast'::character varying)::text, ('coverage_fuzzing'::character varying)::text, ('secret_detection'::character varying)::text]))); diff --git a/lib/gitlab/ci/variables/builder.rb b/lib/gitlab/ci/variables/builder.rb index cae3a966bc61eef257a889c81d2d226905cc553f..d909a9133b16718be898078616c28b135feb88a3 100644 --- a/lib/gitlab/ci/variables/builder.rb +++ b/lib/gitlab/ci/variables/builder.rb @@ -139,6 +139,10 @@ def predefined_variables(job) # Set environment name here so we can access it when evaluating the job's rules variables.append(key: 'CI_ENVIRONMENT_NAME', value: job.environment) if job.environment + + if job.pages_merge_request_preview_generator? + variables.append(key: 'CI_PAGES_MERGE_REQUEST_PREVIEW_URL', value: job.pages_merge_request_preview_url) + end end end diff --git a/lib/gitlab/event_store.rb b/lib/gitlab/event_store.rb index ce71ee594f24b9dd63024ac4d7d6fc7ba287a455..db2c5a9fa84e651ccb265415f9a419e1f88fad4d 100644 --- a/lib/gitlab/event_store.rb +++ b/lib/gitlab/event_store.rb @@ -56,6 +56,10 @@ def self.configure!(store) store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::PagesDomains::PagesDomainUpdatedEvent store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::PagesDomains::PagesDomainCreatedEvent + store.subscribe ::Pages::DeleteMergeRequestPreviewWorker, to: ::Pages::MergeRequestPreviewOutdatedEvent + store.subscribe ::Pages::DeleteMergeRequestPreviewWorker, to: ::MergeRequests::ApprovedEvent + store.subscribe ::Pages::DeleteMergeRequestPreviewWorker, to: ::MergeRequests::ClosedEvent + store.subscribe ::MergeRequests::CreateApprovalEventWorker, to: ::MergeRequests::ApprovedEvent store.subscribe ::MergeRequests::CreateApprovalNoteWorker, to: ::MergeRequests::ApprovedEvent store.subscribe ::MergeRequests::ResolveTodosAfterApprovalWorker, to: ::MergeRequests::ApprovedEvent diff --git a/lib/gitlab/pages/virtual_host_finder.rb b/lib/gitlab/pages/virtual_host_finder.rb index 5fec60188f8513d7365261596750a965e1037f12..7e8875c8123e52cf27d5c4d0e923ceff24fd3404 100644 --- a/lib/gitlab/pages/virtual_host_finder.rb +++ b/lib/gitlab/pages/virtual_host_finder.rb @@ -16,7 +16,8 @@ def execute name = host.delete_suffix(gitlab_host) by_namespace_domain(name) || - by_unique_domain(name) + by_unique_domain(name) || + by_review_app_domain(name) else by_custom_domain(host) end @@ -26,6 +27,20 @@ def execute attr_reader :host + def by_review_app_domain(name) + review_app = ::Pages::MergeRequestPreview.with_deployment.find_by_slug(name) + + return if review_app.blank? + + ::Pages::VirtualDomain.new( + projects: [review_app.project], + lookup_paths: [::Pages::LookupPath.new( + review_app.project, + deployment: review_app.deployment + )] + ) + end + def by_unique_domain(name) project = Project.by_pages_enabled_unique_domain(name)