summaryrefslogtreecommitdiff
path: root/app/services
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-11-18 13:16:36 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-18 13:16:36 +0000
commit311b0269b4eb9839fa63f80c8d7a58f32b8138a0 (patch)
tree07e7870bca8aed6d61fdcc810731c50d2c40af47 /app/services
parent27909cef6c4170ed9205afa7426b8d3de47cbb0c (diff)
downloadgitlab-ce-311b0269b4eb9839fa63f80c8d7a58f32b8138a0.tar.gz
Add latest changes from gitlab-org/gitlab@14-5-stable-eev14.5.0-rc42
Diffstat (limited to 'app/services')
-rw-r--r--app/services/alert_management/process_prometheus_alert_service.rb7
-rw-r--r--app/services/audit_event_service.rb5
-rw-r--r--app/services/authorized_project_update/project_access_changed_service.rb19
-rw-r--r--app/services/award_emojis/base_service.rb2
-rw-r--r--app/services/base_service.rb8
-rw-r--r--app/services/bulk_imports/file_download_service.rb12
-rw-r--r--app/services/bulk_update_integration_service.rb7
-rw-r--r--app/services/ci/create_pipeline_service.rb1
-rw-r--r--app/services/ci/destroy_pipeline_service.rb4
-rw-r--r--app/services/ci/external_pull_requests/create_pipeline_service.rb11
-rw-r--r--app/services/ci/generate_kubeconfig_service.rb62
-rw-r--r--app/services/ci/job_artifacts/create_service.rb41
-rw-r--r--app/services/ci/job_artifacts/destroy_all_expired_service.rb18
-rw-r--r--app/services/ci/job_artifacts/destroy_batch_service.rb23
-rw-r--r--app/services/ci/parse_dotenv_artifact_service.rb19
-rw-r--r--app/services/ci/retry_build_service.rb7
-rw-r--r--app/services/ci/unlock_artifacts_service.rb100
-rw-r--r--app/services/ci/update_build_state_service.rb4
-rw-r--r--app/services/clusters/agents/refresh_authorization_service.rb2
-rw-r--r--app/services/clusters/cleanup/project_namespace_service.rb6
-rw-r--r--app/services/clusters/cleanup/service_account_service.rb5
-rw-r--r--app/services/clusters/integrations/prometheus_health_check_service.rb (renamed from app/services/clusters/applications/prometheus_health_check_service.rb)32
-rw-r--r--app/services/concerns/alert_management/responses.rb26
-rw-r--r--app/services/concerns/issues/issue_type_helpers.rb12
-rw-r--r--app/services/concerns/members/bulk_create_users.rb6
-rw-r--r--app/services/customer_relations/contacts/base_service.rb2
-rw-r--r--app/services/customer_relations/organizations/base_service.rb2
-rw-r--r--app/services/dependency_proxy/find_or_create_blob_service.rb3
-rw-r--r--app/services/dependency_proxy/find_or_create_manifest_service.rb30
-rw-r--r--app/services/dependency_proxy/head_manifest_service.rb5
-rw-r--r--app/services/dependency_proxy/pull_manifest_service.rb8
-rw-r--r--app/services/deployments/archive_in_project_service.rb27
-rw-r--r--app/services/deployments/link_merge_requests_service.rb2
-rw-r--r--app/services/design_management/copy_design_collection/copy_service.rb12
-rw-r--r--app/services/emails/destroy_service.rb2
-rw-r--r--app/services/error_tracking/collect_error_service.rb15
-rw-r--r--app/services/google_cloud/service_accounts_service.rb40
-rw-r--r--app/services/groups/create_service.rb12
-rw-r--r--app/services/groups/import_export/import_service.rb2
-rw-r--r--app/services/groups/transfer_service.rb19
-rw-r--r--app/services/import/github/notes/create_service.rb15
-rw-r--r--app/services/issuable/clone/attributes_rewriter.rb2
-rw-r--r--app/services/issuable_links/list_service.rb7
-rw-r--r--app/services/issues/base_service.rb9
-rw-r--r--app/services/issues/build_service.rb2
-rw-r--r--app/services/issues/close_service.rb9
-rw-r--r--app/services/issues/create_service.rb6
-rw-r--r--app/services/issues/set_crm_contacts_service.rb90
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/jira/requests/base.rb20
-rw-r--r--app/services/labels/transfer_service.rb35
-rw-r--r--app/services/loose_foreign_keys/batch_cleaner_service.rb61
-rw-r--r--app/services/loose_foreign_keys/cleaner_service.rb99
-rw-r--r--app/services/loose_foreign_keys/process_deleted_records_service.rb74
-rw-r--r--app/services/members/create_service.rb19
-rw-r--r--app/services/members/creator_service.rb18
-rw-r--r--app/services/members/invite_service.rb5
-rw-r--r--app/services/merge_requests/outdated_discussion_diff_lines_service.rb61
-rw-r--r--app/services/merge_requests/retarget_chain_service.rb2
-rw-r--r--app/services/merge_requests/toggle_attention_requested_service.rb49
-rw-r--r--app/services/namespaces/in_product_marketing_email_records.rb26
-rw-r--r--app/services/namespaces/in_product_marketing_emails_service.rb31
-rw-r--r--app/services/namespaces/invite_team_email_service.rb62
-rw-r--r--app/services/notes/create_service.rb6
-rw-r--r--app/services/notification_service.rb4
-rw-r--r--app/services/packages/create_dependency_service.rb10
-rw-r--r--app/services/packages/npm/create_package_service.rb10
-rw-r--r--app/services/packages/nuget/create_dependency_service.rb2
-rw-r--r--app/services/packages/rubygems/create_dependencies_service.rb2
-rw-r--r--app/services/packages/update_tags_service.rb2
-rw-r--r--app/services/projects/alerting/notify_service.rb15
-rw-r--r--app/services/projects/all_issues_count_service.rb15
-rw-r--r--app/services/projects/all_merge_requests_count_service.rb15
-rw-r--r--app/services/projects/container_repository/cache_tags_created_at_service.rb70
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb5
-rw-r--r--app/services/projects/create_service.rb10
-rw-r--r--app/services/projects/destroy_service.rb9
-rw-r--r--app/services/projects/detect_repository_languages_service.rb2
-rw-r--r--app/services/projects/import_export/export_service.rb11
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb2
-rw-r--r--app/services/projects/participants_service.rb10
-rw-r--r--app/services/projects/prometheus/alerts/notify_service.rb19
-rw-r--r--app/services/resource_events/base_synthetic_notes_builder_service.rb12
-rw-r--r--app/services/resource_events/change_labels_service.rb2
-rw-r--r--app/services/resource_events/synthetic_label_notes_builder_service.rb4
-rw-r--r--app/services/resource_events/synthetic_milestone_notes_builder_service.rb4
-rw-r--r--app/services/resource_events/synthetic_state_notes_builder_service.rb4
-rw-r--r--app/services/search_service.rb4
-rw-r--r--app/services/security/ci_configuration/sast_iac_create_service.rb25
-rw-r--r--app/services/snippets/update_service.rb2
-rw-r--r--app/services/spam/spam_verdict_service.rb12
-rw-r--r--app/services/suggestions/create_service.rb2
-rw-r--r--app/services/system_note_service.rb4
-rw-r--r--app/services/system_notes/incident_service.rb6
-rw-r--r--app/services/system_notes/issuables_service.rb8
-rw-r--r--app/services/tasks_to_be_done/base_service.rb55
-rw-r--r--app/services/tasks_to_be_done/create_ci_task_service.rb44
-rw-r--r--app/services/tasks_to_be_done/create_code_task_service.rb52
-rw-r--r--app/services/tasks_to_be_done/create_issues_task_service.rb43
-rw-r--r--app/services/todo_service.rb5
-rw-r--r--app/services/users/destroy_service.rb5
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb6
-rw-r--r--app/services/users/upsert_credit_card_validation_service.rb1
103 files changed, 1513 insertions, 330 deletions
diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb
index 605ab7a1869..1b377a3d367 100644
--- a/app/services/alert_management/process_prometheus_alert_service.rb
+++ b/app/services/alert_management/process_prometheus_alert_service.rb
@@ -4,6 +4,7 @@ module AlertManagement
class ProcessPrometheusAlertService
extend ::Gitlab::Utils::Override
include ::AlertManagement::AlertProcessing
+ include ::AlertManagement::Responses
def initialize(project, payload)
@project = project
@@ -18,7 +19,7 @@ module AlertManagement
complete_post_processing_tasks
- ServiceResponse.success
+ success(alert)
end
private
@@ -40,9 +41,5 @@ module AlertManagement
def resolving_alert?
incoming_payload.resolved?
end
-
- def bad_request
- ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
- end
end
end
diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb
index 558798c830d..563d4a924fc 100644
--- a/app/services/audit_event_service.rb
+++ b/app/services/audit_event_service.rb
@@ -119,6 +119,10 @@ class AuditEventService
event
end
+ def stream_event_to_external_destinations(_event)
+ # Defined in EE
+ end
+
def log_authentication_event_to_database
return unless Gitlab::Database.read_write? && authentication_event?
@@ -130,6 +134,7 @@ class AuditEventService
def save_or_track(event)
event.save!
+ stream_event_to_external_destinations(event)
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, audit_event_type: event.class.to_s)
end
diff --git a/app/services/authorized_project_update/project_access_changed_service.rb b/app/services/authorized_project_update/project_access_changed_service.rb
new file mode 100644
index 00000000000..62bf4ced1ae
--- /dev/null
+++ b/app/services/authorized_project_update/project_access_changed_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module AuthorizedProjectUpdate
+ class ProjectAccessChangedService
+ def initialize(project_ids)
+ @project_ids = Array.wrap(project_ids)
+ end
+
+ def execute(blocking: true)
+ bulk_args = @project_ids.map { |id| [id] }
+
+ if blocking
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.bulk_perform_and_wait(bulk_args)
+ else
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
+ end
+ end
+ end
+end
diff --git a/app/services/award_emojis/base_service.rb b/app/services/award_emojis/base_service.rb
index a677d03a221..626e26d63b5 100644
--- a/app/services/award_emojis/base_service.rb
+++ b/app/services/award_emojis/base_service.rb
@@ -14,7 +14,7 @@ module AwardEmojis
private
def normalize_name(name)
- Gitlab::Emoji.normalize_emoji_name(name)
+ TanukiEmoji.find_by_alpha_code(name)&.name || name
end
# Provide more error state data than what BaseService allows.
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 275ebcc7bcd..c7380768e32 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -6,13 +6,7 @@
# and existing service will use these one by one.
# After all are migrated, we can remove this class.
#
-# New services should consider inheriting from:
-#
-# - BaseContainerService for services scoped by container (project or group)
-# - BaseProjectService for services scoped to projects
-# - BaseGroupService for services scoped to groups
-#
-# or, create a new base class and update this comment.
+# For new services, please see https://docs.gitlab.com/ee/development/reusing_abstractions.html#service-classes
class BaseService
include BaseServiceUtility
include Gitlab::Experiment::Dsl
diff --git a/app/services/bulk_imports/file_download_service.rb b/app/services/bulk_imports/file_download_service.rb
index 9a301c260a9..d08dc72e30b 100644
--- a/app/services/bulk_imports/file_download_service.rb
+++ b/app/services/bulk_imports/file_download_service.rb
@@ -7,8 +7,16 @@ module BulkImports
REMOTE_FILENAME_PATTERN = %r{filename="(?<filename>[^"]+)"}.freeze
FILENAME_SIZE_LIMIT = 255 # chars before the extension
-
- def initialize(configuration:, relative_url:, dir:, file_size_limit:, allowed_content_types:, filename: nil)
+ DEFAULT_FILE_SIZE_LIMIT = 5.gigabytes
+ DEFAULT_ALLOWED_CONTENT_TYPES = %w(application/gzip application/octet-stream).freeze
+
+ def initialize(
+ configuration:,
+ relative_url:,
+ dir:,
+ file_size_limit: DEFAULT_FILE_SIZE_LIMIT,
+ allowed_content_types: DEFAULT_ALLOWED_CONTENT_TYPES,
+ filename: nil)
@configuration = configuration
@relative_url = relative_url
@filename = filename
diff --git a/app/services/bulk_update_integration_service.rb b/app/services/bulk_update_integration_service.rb
index 45465ba3946..29c4d0cc220 100644
--- a/app/services/bulk_update_integration_service.rb
+++ b/app/services/bulk_update_integration_service.rb
@@ -12,7 +12,7 @@ class BulkUpdateIntegrationService
Integration.where(id: batch_ids).update_all(integration_hash)
if integration.data_fields_present?
- integration.data_fields.class.where(service_id: batch_ids).update_all(data_fields_hash)
+ integration.data_fields.class.where(data_fields_foreign_key => batch_ids).update_all(data_fields_hash)
end
end
end
@@ -22,6 +22,11 @@ class BulkUpdateIntegrationService
attr_reader :integration, :batch
+ # service_id or integration_id
+ def data_fields_foreign_key
+ integration.data_fields.class.reflections['integration'].foreign_key
+ end
+
def integration_hash
integration.to_integration_hash.tap { |json| json['inherit_from_id'] = integration.inherit_from_id || integration.id }
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index ba9665555cc..540e8f7b970 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -25,6 +25,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::StopDryRun,
Gitlab::Ci::Pipeline::Chain::Create,
+ Gitlab::Ci::Pipeline::Chain::CreateCrossDatabaseAssociations,
Gitlab::Ci::Pipeline::Chain::Limit::Activity,
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity,
Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines,
diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb
index 476c7523d60..6fbde5d291c 100644
--- a/app/services/ci/destroy_pipeline_service.rb
+++ b/app/services/ci/destroy_pipeline_service.rb
@@ -12,7 +12,9 @@ module Ci
# Ci::Pipeline#destroy triggers `use_fast_destroy :job_artifacts` and
# ci_builds has ON DELETE CASCADE to ci_pipelines. The pipeline, the builds,
# job and pipeline artifacts all get destroyed here.
- pipeline.reset.destroy!
+ ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/345664') do
+ pipeline.reset.destroy!
+ end
ServiceResponse.success(message: 'Pipeline not found')
rescue ActiveRecord::RecordNotFound
diff --git a/app/services/ci/external_pull_requests/create_pipeline_service.rb b/app/services/ci/external_pull_requests/create_pipeline_service.rb
index dd93ca4708e..66127c94d35 100644
--- a/app/services/ci/external_pull_requests/create_pipeline_service.rb
+++ b/app/services/ci/external_pull_requests/create_pipeline_service.rb
@@ -16,14 +16,9 @@ module Ci
private
def create_pipeline_for(pull_request)
- if ::Feature.enabled?(:ci_create_external_pr_pipeline_async, project, default_enabled: :yaml)
- Ci::ExternalPullRequests::CreatePipelineWorker.perform_async(
- project.id, current_user.id, pull_request.id
- )
- else
- Ci::CreatePipelineService.new(project, current_user, create_params(pull_request))
- .execute(:external_pull_request_event, external_pull_request: pull_request)
- end
+ Ci::ExternalPullRequests::CreatePipelineWorker.perform_async(
+ project.id, current_user.id, pull_request.id
+ )
end
def create_params(pull_request)
diff --git a/app/services/ci/generate_kubeconfig_service.rb b/app/services/ci/generate_kubeconfig_service.rb
new file mode 100644
index 00000000000..18f68c0ff09
--- /dev/null
+++ b/app/services/ci/generate_kubeconfig_service.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Ci
+ class GenerateKubeconfigService
+ def initialize(build)
+ @build = build
+ @template = Gitlab::Kubernetes::Kubeconfig::Template.new
+ end
+
+ def execute
+ template.add_cluster(
+ name: cluster_name,
+ url: Gitlab::Kas.tunnel_url
+ )
+
+ agents.each do |agent|
+ user = user_name(agent)
+
+ template.add_user(
+ name: user,
+ token: agent_token(agent)
+ )
+
+ template.add_context(
+ name: context_name(agent),
+ cluster: cluster_name,
+ user: user
+ )
+ end
+
+ template
+ end
+
+ private
+
+ attr_reader :build, :template
+
+ def agents
+ build.pipeline.authorized_cluster_agents
+ end
+
+ def cluster_name
+ 'gitlab'
+ end
+
+ def user_name(agent)
+ ['agent', agent.id].join(delimiter)
+ end
+
+ def context_name(agent)
+ [agent.project.full_path, agent.name].join(delimiter)
+ end
+
+ def agent_token(agent)
+ ['ci', agent.id, build.token].join(delimiter)
+ end
+
+ def delimiter
+ ':'
+ end
+ end
+end
diff --git a/app/services/ci/job_artifacts/create_service.rb b/app/services/ci/job_artifacts/create_service.rb
index 9fc7c3b4d40..7c67a2e175d 100644
--- a/app/services/ci/job_artifacts/create_service.rb
+++ b/app/services/ci/job_artifacts/create_service.rb
@@ -19,6 +19,7 @@ module Ci
def initialize(job)
@job = job
@project = job.project
+ @pipeline = job.pipeline if ::Feature.enabled?(:ci_update_unlocked_job_artifacts, @project)
end
def authorize(artifact_type:, filesize: nil)
@@ -53,7 +54,7 @@ module Ci
private
- attr_reader :job, :project
+ attr_reader :job, :project, :pipeline
def validate_requirements(artifact_type:, filesize:)
return too_large_error if too_large?(artifact_type, filesize)
@@ -85,34 +86,38 @@ module Ci
expire_in = params['expire_in'] ||
Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in
- artifact = Ci::JobArtifact.new(
+ artifact_attributes = {
job_id: job.id,
project: project,
- file: artifacts_file,
- file_type: params[:artifact_type],
- file_format: params[:artifact_format],
- file_sha256: artifacts_file.sha256,
- expire_in: expire_in)
+ expire_in: expire_in
+ }
+
+ artifact_attributes[:locked] = pipeline.locked if ::Feature.enabled?(:ci_update_unlocked_job_artifacts, project)
+
+ artifact = Ci::JobArtifact.new(
+ artifact_attributes.merge(
+ file: artifacts_file,
+ file_type: params[:artifact_type],
+ file_format: params[:artifact_format],
+ file_sha256: artifacts_file.sha256
+ )
+ )
artifact_metadata = if metadata_file
Ci::JobArtifact.new(
- job_id: job.id,
- project: project,
- file: metadata_file,
- file_type: :metadata,
- file_format: :gzip,
- file_sha256: metadata_file.sha256,
- expire_in: expire_in)
+ artifact_attributes.merge(
+ file: metadata_file,
+ file_type: :metadata,
+ file_format: :gzip,
+ file_sha256: metadata_file.sha256
+ )
+ )
end
[artifact, artifact_metadata]
end
def parse_artifact(artifact)
- unless Feature.enabled?(:ci_synchronous_artifact_parsing, project, default_enabled: true)
- return success
- end
-
case artifact.file_type
when 'dotenv' then parse_dotenv_artifact(artifact)
else success
diff --git a/app/services/ci/job_artifacts/destroy_all_expired_service.rb b/app/services/ci/job_artifacts/destroy_all_expired_service.rb
index 3e9cc95d135..e4f65736a58 100644
--- a/app/services/ci/job_artifacts/destroy_all_expired_service.rb
+++ b/app/services/ci/job_artifacts/destroy_all_expired_service.rb
@@ -24,7 +24,11 @@ module Ci
# which is scheduled every 7 minutes.
def execute
in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
- destroy_job_artifacts_with_slow_iteration(Time.current)
+ if ::Feature.enabled?(:ci_destroy_unlocked_job_artifacts)
+ destroy_unlocked_job_artifacts(Time.current)
+ else
+ destroy_job_artifacts_with_slow_iteration(Time.current)
+ end
end
@removed_artifacts_count
@@ -32,13 +36,21 @@ module Ci
private
+ def destroy_unlocked_job_artifacts(start_at)
+ loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
+ artifacts = Ci::JobArtifact.expired_before(start_at).artifact_unlocked.limit(BATCH_SIZE)
+ service_response = destroy_batch(artifacts)
+ @removed_artifacts_count += service_response[:destroyed_artifacts_count]
+ end
+ end
+
def destroy_job_artifacts_with_slow_iteration(start_at)
Ci::JobArtifact.expired_before(start_at).each_batch(of: BATCH_SIZE, column: :expire_at, order: :desc) do |relation, index|
# For performance reasons, join with ci_pipelines after the batch is queried.
# See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47496
artifacts = relation.unlocked
- service_response = destroy_batch_async(artifacts)
+ service_response = destroy_batch(artifacts)
@removed_artifacts_count += service_response[:destroyed_artifacts_count]
break if loop_timeout?(start_at)
@@ -46,7 +58,7 @@ module Ci
end
end
- def destroy_batch_async(artifacts)
+ def destroy_batch(artifacts)
Ci::JobArtifacts::DestroyBatchService.new(artifacts).execute
end
diff --git a/app/services/ci/job_artifacts/destroy_batch_service.rb b/app/services/ci/job_artifacts/destroy_batch_service.rb
index 8536b88ccc0..866b40c32d8 100644
--- a/app/services/ci/job_artifacts/destroy_batch_service.rb
+++ b/app/services/ci/job_artifacts/destroy_batch_service.rb
@@ -26,15 +26,18 @@ module Ci
def execute(update_stats: true)
return success(destroyed_artifacts_count: 0, statistics_updates: {}) if @job_artifacts.empty?
+ destroy_related_records(@job_artifacts)
+
Ci::DeletedObject.transaction do
Ci::DeletedObject.bulk_import(@job_artifacts, @pick_up_at)
Ci::JobArtifact.id_in(@job_artifacts.map(&:id)).delete_all
- destroy_related_records(@job_artifacts)
end
+ after_batch_destroy_hook(@job_artifacts)
+
# This is executed outside of the transaction because it depends on Redis
update_project_statistics! if update_stats
- increment_monitoring_statistics(artifacts_count)
+ increment_monitoring_statistics(artifacts_count, artifacts_bytes)
success(destroyed_artifacts_count: artifacts_count,
statistics_updates: affected_project_statistics)
@@ -43,9 +46,12 @@ module Ci
private
- # This method is implemented in EE and it must do only database work
+ # Overriden in EE
def destroy_related_records(artifacts); end
+ # Overriden in EE
+ def after_batch_destroy_hook(artifacts); end
+
# using ! here since this can't be called inside a transaction
def update_project_statistics!
affected_project_statistics.each do |project, delta|
@@ -63,8 +69,9 @@ module Ci
end
end
- def increment_monitoring_statistics(size)
- metrics.increment_destroyed_artifacts(size)
+ def increment_monitoring_statistics(size, bytes)
+ metrics.increment_destroyed_artifacts_count(size)
+ metrics.increment_destroyed_artifacts_bytes(bytes)
end
def metrics
@@ -76,6 +83,12 @@ module Ci
@job_artifacts.count
end
end
+
+ def artifacts_bytes
+ strong_memoize(:artifacts_bytes) do
+ @job_artifacts.sum { |artifact| artifact.try(:size) || 0 }
+ end
+ end
end
end
end
diff --git a/app/services/ci/parse_dotenv_artifact_service.rb b/app/services/ci/parse_dotenv_artifact_service.rb
index 2ee9be476bb..725ecbcce5d 100644
--- a/app/services/ci/parse_dotenv_artifact_service.rb
+++ b/app/services/ci/parse_dotenv_artifact_service.rb
@@ -2,8 +2,7 @@
module Ci
class ParseDotenvArtifactService < ::BaseService
- MAX_ACCEPTABLE_DOTENV_SIZE = 5.kilobytes
- MAX_ACCEPTABLE_VARIABLES_COUNT = 20
+ include ::Gitlab::Utils::StrongMemoize
SizeLimitError = Class.new(StandardError)
ParserError = Class.new(StandardError)
@@ -27,9 +26,9 @@ module Ci
raise ArgumentError, 'Artifact is not dotenv file type'
end
- unless artifact.file.size < MAX_ACCEPTABLE_DOTENV_SIZE
+ unless artifact.file.size < dotenv_size_limit
raise SizeLimitError,
- "Dotenv Artifact Too Big. Maximum Allowable Size: #{MAX_ACCEPTABLE_DOTENV_SIZE}"
+ "Dotenv Artifact Too Big. Maximum Allowable Size: #{dotenv_size_limit}"
end
end
@@ -45,9 +44,9 @@ module Ci
end
end
- if variables.size > MAX_ACCEPTABLE_VARIABLES_COUNT
+ if variables.size > dotenv_variable_limit
raise SizeLimitError,
- "Dotenv files cannot have more than #{MAX_ACCEPTABLE_VARIABLES_COUNT} variables"
+ "Dotenv files cannot have more than #{dotenv_variable_limit} variables"
end
variables
@@ -60,5 +59,13 @@ module Ci
result.each(&:strip!)
end
+
+ def dotenv_variable_limit
+ strong_memoize(:dotenv_variable_limit) { project.actual_limits.dotenv_variables }
+ end
+
+ def dotenv_size_limit
+ strong_memoize(:dotenv_size_limit) { project.actual_limits.dotenv_size }
+ end
end
end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 07cfbb9ce3c..ebb07de9d29 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -63,7 +63,7 @@ module Ci
def clone_build(build)
project.builds.new(build_attributes(build)).tap do |new_build|
- new_build.assign_attributes(::Gitlab::Ci::Pipeline::Seed::Build.environment_attributes_for(new_build))
+ new_build.assign_attributes(deployment_attributes_for(new_build, build))
end
end
@@ -75,6 +75,11 @@ module Ci
attributes[:user] = current_user
attributes
end
+
+ def deployment_attributes_for(new_build, old_build)
+ ::Gitlab::Ci::Pipeline::Seed::Build
+ .deployment_attributes_for(new_build, old_build.persisted_environment)
+ end
end
end
diff --git a/app/services/ci/unlock_artifacts_service.rb b/app/services/ci/unlock_artifacts_service.rb
index 7c169cb8395..30da31ba8ec 100644
--- a/app/services/ci/unlock_artifacts_service.rb
+++ b/app/services/ci/unlock_artifacts_service.rb
@@ -5,22 +5,84 @@ module Ci
BATCH_SIZE = 100
def execute(ci_ref, before_pipeline = nil)
- query = <<~SQL.squish
- UPDATE "ci_pipelines"
- SET "locked" = #{::Ci::Pipeline.lockeds[:unlocked]}
- WHERE "ci_pipelines"."id" in (
- #{collect_pipelines(ci_ref, before_pipeline).select(:id).to_sql}
- LIMIT #{BATCH_SIZE}
- FOR UPDATE SKIP LOCKED
- )
- RETURNING "ci_pipelines"."id";
- SQL
-
- loop do
- break if Ci::Pipeline.connection.exec_query(query).empty?
+ results = {
+ unlocked_pipelines: 0,
+ unlocked_job_artifacts: 0
+ }
+
+ if ::Feature.enabled?(:ci_update_unlocked_job_artifacts, ci_ref.project)
+ loop do
+ unlocked_pipelines = []
+ unlocked_job_artifacts = []
+
+ ::Ci::Pipeline.transaction do
+ unlocked_pipelines = unlock_pipelines(ci_ref, before_pipeline)
+ unlocked_job_artifacts = unlock_job_artifacts(unlocked_pipelines)
+ end
+
+ break if unlocked_pipelines.empty?
+
+ results[:unlocked_pipelines] += unlocked_pipelines.length
+ results[:unlocked_job_artifacts] += unlocked_job_artifacts.length
+ end
+ else
+ query = <<~SQL.squish
+ UPDATE "ci_pipelines"
+ SET "locked" = #{::Ci::Pipeline.lockeds[:unlocked]}
+ WHERE "ci_pipelines"."id" in (
+ #{collect_pipelines(ci_ref, before_pipeline).select(:id).to_sql}
+ LIMIT #{BATCH_SIZE}
+ FOR UPDATE SKIP LOCKED
+ )
+ RETURNING "ci_pipelines"."id";
+ SQL
+
+ loop do
+ unlocked_pipelines = Ci::Pipeline.connection.exec_query(query)
+
+ break if unlocked_pipelines.empty?
+
+ results[:unlocked_pipelines] += unlocked_pipelines.length
+ end
end
+
+ results
end
+ # rubocop:disable CodeReuse/ActiveRecord
+ def unlock_job_artifacts_query(pipeline_ids)
+ ci_job_artifacts = ::Ci::JobArtifact.arel_table
+
+ build_ids = ::Ci::Build.select(:id).where(commit_id: pipeline_ids)
+
+ returning = Arel::Nodes::Grouping.new(ci_job_artifacts[:id])
+
+ Arel::UpdateManager.new
+ .table(ci_job_artifacts)
+ .where(ci_job_artifacts[:job_id].in(Arel.sql(build_ids.to_sql)))
+ .set([[ci_job_artifacts[:locked], ::Ci::JobArtifact.lockeds[:unlocked]]])
+ .to_sql + " RETURNING #{returning.to_sql}"
+ end
+ # rubocop:enable CodeReuse/ActiveRecord
+
+ # rubocop:disable CodeReuse/ActiveRecord
+ def unlock_pipelines_query(ci_ref, before_pipeline)
+ ci_pipelines = ::Ci::Pipeline.arel_table
+
+ pipelines_scope = ci_ref.pipelines.artifacts_locked
+ pipelines_scope = pipelines_scope.before_pipeline(before_pipeline) if before_pipeline
+ pipelines_scope = pipelines_scope.select(:id).limit(BATCH_SIZE).lock('FOR UPDATE SKIP LOCKED')
+
+ returning = Arel::Nodes::Grouping.new(ci_pipelines[:id])
+
+ Arel::UpdateManager.new
+ .table(ci_pipelines)
+ .where(ci_pipelines[:id].in(Arel.sql(pipelines_scope.to_sql)))
+ .set([[ci_pipelines[:locked], ::Ci::Pipeline.lockeds[:unlocked]]])
+ .to_sql + " RETURNING #{returning.to_sql}"
+ end
+ # rubocop:enable CodeReuse/ActiveRecord
+
private
def collect_pipelines(ci_ref, before_pipeline)
@@ -29,5 +91,17 @@ module Ci
pipeline_scope.artifacts_locked
end
+
+ def unlock_job_artifacts(pipelines)
+ return if pipelines.empty?
+
+ ::Ci::JobArtifact.connection.exec_query(
+ unlock_job_artifacts_query(pipelines.rows.flatten)
+ )
+ end
+
+ def unlock_pipelines(ci_ref, before_pipeline)
+ ::Ci::Pipeline.connection.exec_query(unlock_pipelines_query(ci_ref, before_pipeline))
+ end
end
end
diff --git a/app/services/ci/update_build_state_service.rb b/app/services/ci/update_build_state_service.rb
index 3b403f92486..826d9a2eda3 100644
--- a/app/services/ci/update_build_state_service.rb
+++ b/app/services/ci/update_build_state_service.rb
@@ -73,11 +73,11 @@ module Ci
::Gitlab::Ci::Trace::Checksum.new(build).then do |checksum|
unless checksum.valid?
metrics.increment_trace_operation(operation: :invalid)
- metrics.increment_error_counter(type: :chunks_invalid_checksum)
+ metrics.increment_error_counter(error_reason: :chunks_invalid_checksum)
if checksum.corrupted?
metrics.increment_trace_operation(operation: :corrupted)
- metrics.increment_error_counter(type: :chunks_invalid_size)
+ metrics.increment_error_counter(error_reason: :chunks_invalid_size)
end
next unless log_invalid_chunks?
diff --git a/app/services/clusters/agents/refresh_authorization_service.rb b/app/services/clusters/agents/refresh_authorization_service.rb
index 7f401eef720..54b90a7304c 100644
--- a/app/services/clusters/agents/refresh_authorization_service.rb
+++ b/app/services/clusters/agents/refresh_authorization_service.rb
@@ -86,7 +86,7 @@ module Clusters
if group_root_ancestor?
root_ancestor.all_projects
else
- ::Project.none
+ ::Project.id_in(project.id)
end
end
diff --git a/app/services/clusters/cleanup/project_namespace_service.rb b/app/services/clusters/cleanup/project_namespace_service.rb
index 16254041306..0173f93f625 100644
--- a/app/services/clusters/cleanup/project_namespace_service.rb
+++ b/app/services/clusters/cleanup/project_namespace_service.rb
@@ -35,9 +35,11 @@ module Clusters
end
def kubeclient_delete_namespace(kubernetes_namespace)
- cluster.kubeclient.delete_namespace(kubernetes_namespace.namespace)
+ cluster.kubeclient&.delete_namespace(kubernetes_namespace.namespace)
rescue Kubeclient::ResourceNotFoundError
- # no-op: nothing to delete
+ # The resources have already been deleted, possibly on a previous attempt that timed out
+ rescue Gitlab::UrlBlocker::BlockedUrlError
+ # User gave an invalid cluster from the start, or deleted the endpoint before this job ran
end
end
end
diff --git a/app/services/clusters/cleanup/service_account_service.rb b/app/services/clusters/cleanup/service_account_service.rb
index baac9e4a9e7..53f968cd409 100644
--- a/app/services/clusters/cleanup/service_account_service.rb
+++ b/app/services/clusters/cleanup/service_account_service.rb
@@ -16,11 +16,14 @@ module Clusters
def delete_gitlab_service_account
log_event(:deleting_gitlab_service_account)
- cluster.kubeclient.delete_service_account(
+ cluster.kubeclient&.delete_service_account(
::Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAME,
::Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE
)
rescue Kubeclient::ResourceNotFoundError
+ # The resources have already been deleted, possibly on a previous attempt that timed out
+ rescue Gitlab::UrlBlocker::BlockedUrlError
+ # User gave an invalid cluster from the start, or deleted the endpoint before this job ran
end
end
end
diff --git a/app/services/clusters/applications/prometheus_health_check_service.rb b/app/services/clusters/integrations/prometheus_health_check_service.rb
index eda47f56e72..cd06e59449c 100644
--- a/app/services/clusters/applications/prometheus_health_check_service.rb
+++ b/app/services/clusters/integrations/prometheus_health_check_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Clusters
- module Applications
+ module Integrations
class PrometheusHealthCheckService
include Gitlab::Utils::StrongMemoize
include Gitlab::Routing
@@ -14,7 +14,7 @@ module Clusters
def execute
raise 'Invalid cluster type. Only project types are allowed.' unless @cluster.project_type?
- return unless prometheus_application.installed?
+ return unless prometheus_integration.enabled
project = @cluster.clusterable
@@ -28,32 +28,46 @@ module Clusters
send_notification(project) if became_unhealthy?
- prometheus_application.update_columns(healthy: currently_healthy?) if health_changed?
+ prometheus_integration.update_columns(health_status: current_health_status) if health_changed?
end
private
- def prometheus_application
- strong_memoize(:prometheus_application) do
- @cluster.application_prometheus
+ def prometheus_integration
+ strong_memoize(:prometheus_integration) do
+ @cluster.integration_prometheus
+ end
+ end
+
+ def current_health_status
+ if currently_healthy?
+ :healthy
+ else
+ :unhealthy
end
end
def currently_healthy?
strong_memoize(:currently_healthy) do
- prometheus_application.prometheus_client.healthy?
+ prometheus_integration.prometheus_client.healthy?
end
end
def became_unhealthy?
strong_memoize(:became_unhealthy) do
- (was_healthy? || was_healthy?.nil?) && !currently_healthy?
+ (was_healthy? || was_unknown?) && !currently_healthy?
end
end
def was_healthy?
strong_memoize(:was_healthy) do
- prometheus_application.healthy
+ prometheus_integration.healthy?
+ end
+ end
+
+ def was_unknown?
+ strong_memoize(:was_unknown) do
+ prometheus_integration.unknown?
end
end
diff --git a/app/services/concerns/alert_management/responses.rb b/app/services/concerns/alert_management/responses.rb
new file mode 100644
index 00000000000..183a831a00a
--- /dev/null
+++ b/app/services/concerns/alert_management/responses.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ # Module to hold common response logic for AlertManagement services.
+ module Responses
+ def success(alerts)
+ ServiceResponse.success(payload: { alerts: Array(alerts) })
+ end
+
+ def bad_request
+ ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
+ end
+
+ def unauthorized
+ ServiceResponse.error(message: 'Unauthorized', http_status: :unauthorized)
+ end
+
+ def unprocessable_entity
+ ServiceResponse.error(message: 'Unprocessable Entity', http_status: :unprocessable_entity)
+ end
+
+ def forbidden
+ ServiceResponse.error(message: 'Forbidden', http_status: :forbidden)
+ end
+ end
+end
diff --git a/app/services/concerns/issues/issue_type_helpers.rb b/app/services/concerns/issues/issue_type_helpers.rb
new file mode 100644
index 00000000000..44c20d20ff1
--- /dev/null
+++ b/app/services/concerns/issues/issue_type_helpers.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Issues
+ module IssueTypeHelpers
+ # @param object [Issue, Project]
+ # @param issue_type [String, Symbol]
+ def create_issue_type_allowed?(object, issue_type)
+ WorkItem::Type.base_types.key?(issue_type.to_s) &&
+ can?(current_user, :"create_#{issue_type}", object)
+ end
+ end
+end
diff --git a/app/services/concerns/members/bulk_create_users.rb b/app/services/concerns/members/bulk_create_users.rb
index 4498f40c396..b98917f1396 100644
--- a/app/services/concerns/members/bulk_create_users.rb
+++ b/app/services/concerns/members/bulk_create_users.rb
@@ -6,7 +6,7 @@ module Members
included do
class << self
- def add_users(source, users, access_level, current_user: nil, expires_at: nil)
+ def add_users(source, users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
return [] unless users.present?
emails, users, existing_members = parse_users_list(source, users)
@@ -18,7 +18,9 @@ module Members
access_level,
existing_members: existing_members,
current_user: current_user,
- expires_at: expires_at)
+ expires_at: expires_at,
+ tasks_to_be_done: tasks_to_be_done,
+ tasks_project_id: tasks_project_id)
.execute
end
end
diff --git a/app/services/customer_relations/contacts/base_service.rb b/app/services/customer_relations/contacts/base_service.rb
index 89f6f2c3f1f..1797e5021a1 100644
--- a/app/services/customer_relations/contacts/base_service.rb
+++ b/app/services/customer_relations/contacts/base_service.rb
@@ -6,7 +6,7 @@ module CustomerRelations
private
def allowed?
- current_user&.can?(:admin_contact, group)
+ current_user&.can?(:admin_crm_contact, group)
end
def error(message)
diff --git a/app/services/customer_relations/organizations/base_service.rb b/app/services/customer_relations/organizations/base_service.rb
index 8f8480d697c..9b4ccafcea6 100644
--- a/app/services/customer_relations/organizations/base_service.rb
+++ b/app/services/customer_relations/organizations/base_service.rb
@@ -6,7 +6,7 @@ module CustomerRelations
private
def allowed?
- current_user&.can?(:admin_organization, group)
+ current_user&.can?(:admin_crm_organization, group)
end
def error(message)
diff --git a/app/services/dependency_proxy/find_or_create_blob_service.rb b/app/services/dependency_proxy/find_or_create_blob_service.rb
index 0a6db6e3d34..1b43263a3ba 100644
--- a/app/services/dependency_proxy/find_or_create_blob_service.rb
+++ b/app/services/dependency_proxy/find_or_create_blob_service.rb
@@ -30,8 +30,7 @@ module DependencyProxy
blob.save!
end
- # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
- blob.touch if from_cache
+ blob.read! if from_cache
success(blob: blob, from_cache: from_cache)
end
diff --git a/app/services/dependency_proxy/find_or_create_manifest_service.rb b/app/services/dependency_proxy/find_or_create_manifest_service.rb
index 1976d4d47f4..aeb62be9f3a 100644
--- a/app/services/dependency_proxy/find_or_create_manifest_service.rb
+++ b/app/services/dependency_proxy/find_or_create_manifest_service.rb
@@ -14,18 +14,18 @@ module DependencyProxy
def execute
@manifest = @group.dependency_proxy_manifests
.active
- .find_or_initialize_by_file_name_or_digest(file_name: @file_name, digest: @tag)
+ .find_by_file_name_or_digest(file_name: @file_name, digest: @tag)
head_result = DependencyProxy::HeadManifestService.new(@image, @tag, @token).execute
- if cached_manifest_matches?(head_result)
- @manifest.touch
+ return respond if cached_manifest_matches?(head_result)
- return success(manifest: @manifest, from_cache: true)
+ if Feature.enabled?(:dependency_proxy_manifest_workhorse, @group, default_enabled: :yaml)
+ success(manifest: nil, from_cache: false)
+ else
+ pull_new_manifest
+ respond(from_cache: false)
end
-
- pull_new_manifest
- respond(from_cache: false)
rescue Timeout::Error, *Gitlab::HTTP::HTTP_ERRORS
respond
end
@@ -34,12 +34,19 @@ module DependencyProxy
def pull_new_manifest
DependencyProxy::PullManifestService.new(@image, @tag, @token).execute_with_manifest do |new_manifest|
- @manifest.update!(
+ params = {
+ file_name: @file_name,
content_type: new_manifest[:content_type],
digest: new_manifest[:digest],
file: new_manifest[:file],
size: new_manifest[:file].size
- )
+ }
+
+ if @manifest
+ @manifest.update!(params)
+ else
+ @manifest = @group.dependency_proxy_manifests.create!(params)
+ end
end
end
@@ -50,9 +57,8 @@ module DependencyProxy
end
def respond(from_cache: true)
- if @manifest.persisted?
- # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
- @manifest.touch if from_cache
+ if @manifest
+ @manifest.read!
success(manifest: @manifest, from_cache: from_cache)
else
diff --git a/app/services/dependency_proxy/head_manifest_service.rb b/app/services/dependency_proxy/head_manifest_service.rb
index ecc3eb77399..cd575b83a98 100644
--- a/app/services/dependency_proxy/head_manifest_service.rb
+++ b/app/services/dependency_proxy/head_manifest_service.rb
@@ -14,7 +14,10 @@ module DependencyProxy
response = Gitlab::HTTP.head(manifest_url, headers: auth_headers.merge(Accept: ACCEPT_HEADERS))
if response.success?
- success(digest: response.headers['docker-content-digest'], content_type: response.headers['content-type'])
+ success(
+ digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER],
+ content_type: response.headers['content-type']
+ )
else
error(response.body, response.code)
end
diff --git a/app/services/dependency_proxy/pull_manifest_service.rb b/app/services/dependency_proxy/pull_manifest_service.rb
index 31494773cc0..e8f0ad6374a 100644
--- a/app/services/dependency_proxy/pull_manifest_service.rb
+++ b/app/services/dependency_proxy/pull_manifest_service.rb
@@ -20,7 +20,13 @@ module DependencyProxy
file.write(response.body)
file.flush
- yield(success(file: file, digest: response.headers['docker-content-digest'], content_type: response.headers['content-type']))
+ yield(
+ success(
+ file: file,
+ digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER],
+ content_type: response.headers['content-type']
+ )
+ )
ensure
file.close
file.unlink
diff --git a/app/services/deployments/archive_in_project_service.rb b/app/services/deployments/archive_in_project_service.rb
new file mode 100644
index 00000000000..a593721f390
--- /dev/null
+++ b/app/services/deployments/archive_in_project_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Deployments
+ # This service archives old deploymets and deletes deployment refs for
+ # keeping the project repository performant.
+ class ArchiveInProjectService < ::BaseService
+ BATCH_SIZE = 100
+
+ def execute
+ unless ::Feature.enabled?(:deployments_archive, project, default_enabled: :yaml)
+ return error('Feature flag is not enabled')
+ end
+
+ deployments = Deployment.archivables_in(project, limit: BATCH_SIZE)
+
+ return success(result: :empty) if deployments.empty?
+
+ ids = deployments.map(&:id)
+ ref_paths = deployments.map(&:ref_path)
+
+ project.repository.delete_refs(*ref_paths)
+ project.deployments.id_in(ids).update_all(archived: true)
+
+ success(result: :archived, count: ids.count)
+ end
+ end
+end
diff --git a/app/services/deployments/link_merge_requests_service.rb b/app/services/deployments/link_merge_requests_service.rb
index 39fbef5dee2..40385418e48 100644
--- a/app/services/deployments/link_merge_requests_service.rb
+++ b/app/services/deployments/link_merge_requests_service.rb
@@ -16,7 +16,7 @@ module Deployments
# Review apps have the environment type set (e.g. to `review`, though the
# exact value may differ). We don't want to link merge requests to review
# app deployments, as this is not useful.
- return if deployment.environment.environment_type
+ return unless deployment.environment.should_link_to_merge_requests?
# This service is triggered by a Sidekiq worker, which only runs when a
# deployment is successful. We add an extra check here in case we ever
diff --git a/app/services/design_management/copy_design_collection/copy_service.rb b/app/services/design_management/copy_design_collection/copy_service.rb
index c43696442d2..5e557e9ea53 100644
--- a/app/services/design_management/copy_design_collection/copy_service.rb
+++ b/app/services/design_management/copy_design_collection/copy_service.rb
@@ -181,12 +181,12 @@ module DesignManagement
)
end
- # TODO Replace `Gitlab::Database.main.bulk_insert` with `BulkInsertSafe`
+ # TODO Replace `ApplicationRecord.legacy_bulk_insert` with `BulkInsertSafe`
# once https://gitlab.com/gitlab-org/gitlab/-/issues/247718 is fixed.
# When this is fixed, we can remove the call to
# `with_project_iid_supply` above, since the objects will be instantiated
# and callbacks (including `ensure_project_iid!`) will fire.
- ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert( # rubocop:disable Gitlab/BulkInsert
DesignManagement::Design.table_name,
new_rows,
return_ids: true
@@ -207,9 +207,9 @@ module DesignManagement
)
end
- # TODO Replace `Gitlab::Database.main.bulk_insert` with `BulkInsertSafe`
+ # TODO Replace `ApplicationRecord.legacy_bulk_insert` with `BulkInsertSafe`
# once https://gitlab.com/gitlab-org/gitlab/-/issues/247718 is fixed.
- ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert( # rubocop:disable Gitlab/BulkInsert
DesignManagement::Version.table_name,
new_rows,
return_ids: true
@@ -239,7 +239,7 @@ module DesignManagement
end
# We cannot use `BulkInsertSafe` because of the uploader mounted in `Action`.
- ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert( # rubocop:disable Gitlab/BulkInsert
DesignManagement::Action.table_name,
new_rows
)
@@ -278,7 +278,7 @@ module DesignManagement
# We cannot use `BulkInsertSafe` due to the LfsObjectsProject#update_project_statistics
# callback that fires after_commit.
- ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert( # rubocop:disable Gitlab/BulkInsert
LfsObjectsProject.table_name,
new_rows,
on_conflict: :do_nothing # Upsert
diff --git a/app/services/emails/destroy_service.rb b/app/services/emails/destroy_service.rb
index d10833e66cb..d211c3470b2 100644
--- a/app/services/emails/destroy_service.rb
+++ b/app/services/emails/destroy_service.rb
@@ -3,6 +3,8 @@
module Emails
class DestroyService < ::Emails::BaseService
def execute(email)
+ raise StandardError, 'Cannot delete primary email' if email.user_primary_email?
+
email.destroy && update_secondary_emails!(email.email)
end
diff --git a/app/services/error_tracking/collect_error_service.rb b/app/services/error_tracking/collect_error_service.rb
index 477453a693e..304e3898ee5 100644
--- a/app/services/error_tracking/collect_error_service.rb
+++ b/app/services/error_tracking/collect_error_service.rb
@@ -15,7 +15,7 @@ module ErrorTracking
)
# The payload field contains all the data on error including stacktrace in jsonb.
- # Together with occured_at these are 2 main attributes that we need to save here.
+ # Together with occurred_at these are 2 main attributes that we need to save here.
error.events.create!(
environment: event['environment'],
description: exception['value'],
@@ -28,7 +28,18 @@ module ErrorTracking
private
def event
- params[:event]
+ @event ||= format_event(params[:event])
+ end
+
+ def format_event(event)
+ # Some SDK send exception payload as Array. For exmple Go lang SDK.
+ # We need to convert it to hash format we expect.
+ if event['exception'].is_a?(Array)
+ exception = event['exception']
+ event['exception'] = { 'values' => exception }
+ end
+
+ event
end
def exception
diff --git a/app/services/google_cloud/service_accounts_service.rb b/app/services/google_cloud/service_accounts_service.rb
new file mode 100644
index 00000000000..29ed69693b0
--- /dev/null
+++ b/app/services/google_cloud/service_accounts_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module GoogleCloud
+ ##
+ # GCP keys used to store Google Cloud Service Accounts
+ GCP_KEYS = %w[GCP_PROJECT_ID GCP_SERVICE_ACCOUNT GCP_SERVICE_ACCOUNT_KEY].freeze
+
+ ##
+ # This service deals with GCP Service Accounts in GitLab
+
+ class ServiceAccountsService < ::BaseService
+ ##
+ # Find GCP Service Accounts in a GitLab project
+ #
+ # This method looks up GitLab project's CI vars
+ # and returns Google Cloud Service Accounts combinations
+ # aligning GitLab project and environment to GCP projects
+
+ def find_for_project
+ group_vars_by_environment.map do |environment_scope, value|
+ {
+ environment: environment_scope,
+ gcp_project: value['GCP_PROJECT_ID'],
+ service_account_exists: value['GCP_SERVICE_ACCOUNT'].present?,
+ service_account_key_exists: value['GCP_SERVICE_ACCOUNT_KEY'].present?
+ }
+ end
+ end
+
+ private
+
+ def group_vars_by_environment
+ filtered_vars = @project.variables.filter { |variable| GCP_KEYS.include? variable.key }
+ filtered_vars.each_with_object({}) do |variable, grouped|
+ grouped[variable.environment_scope] ||= {}
+ grouped[variable.environment_scope][variable.key] = variable.value
+ end
+ end
+ end
+end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index f900927793a..da3cebc2e6d 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -6,6 +6,7 @@ module Groups
@current_user = user
@params = params.dup
@chat_team = @params.delete(:create_chat_team)
+ @create_event = @params.delete(:create_event)
end
def execute
@@ -42,15 +43,26 @@ module Groups
end
end
+ after_create_hook
+
@group
end
private
+ attr_reader :create_event
+
def after_build_hook(group, params)
# overridden in EE
end
+ def after_create_hook
+ if group.persisted? && group.root?
+ delay = Namespaces::InviteTeamEmailService::DELIVERY_DELAY_IN_MINUTES
+ Namespaces::InviteTeamEmailWorker.perform_in(delay, group.id, current_user.id)
+ end
+ end
+
def remove_unallowed_params
params.delete(:default_branch_protection) unless can?(current_user, :create_group_with_default_branch_protection)
params.delete(:allow_mfa_for_subgroups)
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
index f9db552f743..c8c2124078d 100644
--- a/app/services/groups/import_export/import_service.rb
+++ b/app/services/groups/import_export/import_service.rb
@@ -14,7 +14,7 @@ module Groups
def async_execute
group_import_state = GroupImportState.safe_find_or_create_by!(group: group, user: current_user)
- jid = GroupImportWorker.perform_async(current_user.id, group.id)
+ jid = GroupImportWorker.with_status.perform_async(current_user.id, group.id)
if jid.present?
group_import_state.update!(jid: jid)
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index 334083a859f..cd89eb799dc 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -175,21 +175,18 @@ module Groups
end
def refresh_project_authorizations
- ProjectAuthorization.where(project_id: @group.all_projects.select(:id)).delete_all # rubocop: disable CodeReuse/ActiveRecord
+ projects_to_update = Set.new
- # refresh authorized projects for current_user immediately
- current_user.refresh_authorized_projects
-
- # schedule refreshing projects for all the members of the group
- @group.refresh_members_authorized_projects
+ # All projects in this hierarchy need to have their project authorizations recalculated
+ @group.all_projects.each_batch { |prjs| projects_to_update.merge(prjs.ids) } # rubocop: disable CodeReuse/ActiveRecord
# When a group is transferred, it also affects who gets access to the projects shared to
# the subgroups within its hierarchy, so we also schedule jobs that refresh authorizations for all such shared projects.
- project_group_shares_within_the_hierarchy = ProjectGroupLink.in_group(group.self_and_descendants.select(:id))
-
- project_group_shares_within_the_hierarchy.find_each do |project_group_link|
- AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project_group_link.project_id)
+ ProjectGroupLink.in_group(@group.self_and_descendants.select(:id)).each_batch do |project_group_links|
+ projects_to_update.merge(project_group_links.pluck(:project_id)) # rubocop: disable CodeReuse/ActiveRecord
end
+
+ AuthorizedProjectUpdate::ProjectAccessChangedService.new(projects_to_update.to_a).execute unless projects_to_update.empty?
end
def raise_transfer_error(message)
@@ -199,7 +196,7 @@ module Groups
def localized_error_messages
{
database_not_supported: s_('TransferGroup|Database is not supported.'),
- namespace_with_same_path: s_('TransferGroup|The parent group already has a subgroup with the same path.'),
+ namespace_with_same_path: s_('TransferGroup|The parent group already has a subgroup or a project with the same path.'),
group_is_already_root: s_('TransferGroup|Group is already a root group.'),
same_parent_as_current: s_('TransferGroup|Group is already associated to the parent group.'),
invalid_policies: s_("TransferGroup|You don't have enough permissions."),
diff --git a/app/services/import/github/notes/create_service.rb b/app/services/import/github/notes/create_service.rb
new file mode 100644
index 00000000000..79145f42313
--- /dev/null
+++ b/app/services/import/github/notes/create_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Import
+ module Github
+ module Notes
+ class CreateService < ::Notes::CreateService
+ # Github does not have support to quick actions in notes (like /assign)
+ # Therefore, when importing notes we skip the quick actions processing
+ def quick_actions_supported?(_note)
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb
index d8b639bb422..279d3051848 100644
--- a/app/services/issuable/clone/attributes_rewriter.rb
+++ b/app/services/issuable/clone/attributes_rewriter.rb
@@ -99,7 +99,7 @@ module Issuable
yield(event)
end.compact
- Gitlab::Database.main.bulk_insert(table_name, events) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(table_name, events) # rubocop:disable Gitlab/BulkInsert
end
end
diff --git a/app/services/issuable_links/list_service.rb b/app/services/issuable_links/list_service.rb
index fe9678dcc32..cc41a65379a 100644
--- a/app/services/issuable_links/list_service.rb
+++ b/app/services/issuable_links/list_service.rb
@@ -12,11 +12,16 @@ module IssuableLinks
end
def execute
- serializer.new(current_user: current_user, issuable: issuable).represent(child_issuables)
+ serializer.new(current_user: current_user, issuable: issuable)
+ .represent(child_issuables, serializer_options)
end
private
+ def serializer_options
+ {}
+ end
+
def serializer
raise NotImplementedError
end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 6dce9fd6e73..efb5de5b17c 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -3,6 +3,7 @@
module Issues
class BaseService < ::IssuableBaseService
include IncidentManagement::UsageData
+ include IssueTypeHelpers
def hook_data(issue, action, old_associations: {})
hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
@@ -44,7 +45,7 @@ module Issues
def filter_params(issue)
super
- params.delete(:issue_type) unless issue_type_allowed?(issue)
+ params.delete(:issue_type) unless create_issue_type_allowed?(issue, params[:issue_type])
filter_incident_label(issue) if params[:issue_type]
moved_issue = params.delete(:moved_issue)
@@ -89,12 +90,6 @@ module Issues
Milestones::IssuesCountService.new(milestone).delete_cache
end
- # @param object [Issue, Project]
- def issue_type_allowed?(object)
- WorkItem::Type.base_types.key?(params[:issue_type]) &&
- can?(current_user, :"create_#{params[:issue_type]}", object)
- end
-
# @param issue [Issue]
def filter_incident_label(issue)
return unless add_incident_label?(issue) || remove_incident_label?(issue)
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index 7fdc8daf15c..8fd844c4886 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -80,7 +80,7 @@ module Issues
]
allowed_params << :milestone_id if can?(current_user, :admin_issue, project)
- allowed_params << :issue_type if issue_type_allowed?(project)
+ allowed_params << :issue_type if create_issue_type_allowed?(project, params[:issue_type])
params.slice(*allowed_params)
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index ac846c769a3..65f143d0b21 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -62,6 +62,7 @@ module Issues
def perform_incident_management_actions(issue)
resolve_alert(issue)
+ resolve_incident(issue)
end
def close_external_issue(issue, closed_via)
@@ -91,6 +92,14 @@ module Issues
end
end
+ def resolve_incident(issue)
+ return unless issue.incident?
+
+ status = issue.incident_management_issuable_escalation_status || issue.build_incident_management_issuable_escalation_status
+
+ SystemNoteService.resolve_incident_status(issue, current_user) if status.resolve
+ end
+
def store_first_mentioned_in_commit_at(issue, merge_request, max_commit_lookup: 100)
metrics = issue.metrics
return if metrics.nil? || metrics.first_mentioned_in_commit_at
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index fcedd1c1c8d..fa8d380404b 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -6,7 +6,7 @@ module Issues
prepend RateLimitedService
rate_limit key: :issues_create,
- opts: { scope: [:project, :current_user], users_allowlist: -> { [User.support_bot.username] } }
+ opts: { scope: [:project, :current_user, :external_author] }
# NOTE: For Issues::CreateService, we require the spam_params and do not default it to nil, because
# spam_checking is likely to be necessary. However, if there is not a request available in scope
@@ -25,6 +25,10 @@ module Issues
create(@issue, skip_system_notes: skip_system_notes)
end
+ def external_author
+ params[:external_author] # present when creating an issue using service desk (email: from)
+ end
+
def before_create(issue)
Spam::SpamActionService.new(
spammable: issue,
diff --git a/app/services/issues/set_crm_contacts_service.rb b/app/services/issues/set_crm_contacts_service.rb
new file mode 100644
index 00000000000..13fe30b5ac8
--- /dev/null
+++ b/app/services/issues/set_crm_contacts_service.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+module Issues
+ class SetCrmContactsService < ::BaseProjectService
+ attr_accessor :issue, :errors
+
+ MAX_ADDITIONAL_CONTACTS = 6
+
+ def execute(issue)
+ @issue = issue
+ @errors = []
+
+ return error_no_permissions unless allowed?
+ return error_invalid_params unless valid_params?
+
+ determine_changes if params[:crm_contact_ids]
+
+ return error_too_many if too_many?
+
+ add_contacts if params[:add_crm_contact_ids]
+ remove_contacts if params[:remove_crm_contact_ids]
+
+ if issue.valid?
+ ServiceResponse.success(payload: issue)
+ else
+ # The default error isn't very helpful: "Issue customer relations contacts is invalid"
+ issue.errors.delete(:issue_customer_relations_contacts)
+ issue.errors.add(:issue_customer_relations_contacts, errors.to_sentence)
+ ServiceResponse.error(payload: issue, message: issue.errors.full_messages)
+ end
+ end
+
+ private
+
+ def determine_changes
+ existing_contact_ids = issue.issue_customer_relations_contacts.map(&:contact_id)
+ params[:add_crm_contact_ids] = params[:crm_contact_ids] - existing_contact_ids
+ params[:remove_crm_contact_ids] = existing_contact_ids - params[:crm_contact_ids]
+ end
+
+ def add_contacts
+ params[:add_crm_contact_ids].uniq.each do |contact_id|
+ issue_contact = issue.issue_customer_relations_contacts.create(contact_id: contact_id)
+
+ unless issue_contact.persisted?
+ # The validation ensures that the id exists and the user has permission
+ errors << "#{contact_id}: The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ end
+ end
+ end
+
+ def remove_contacts
+ issue.issue_customer_relations_contacts
+ .where(contact_id: params[:remove_crm_contact_ids]) # rubocop: disable CodeReuse/ActiveRecord
+ .delete_all
+ end
+
+ def allowed?
+ current_user&.can?(:set_issue_crm_contacts, issue)
+ end
+
+ def valid_params?
+ set_present? ^ add_or_remove_present?
+ end
+
+ def set_present?
+ params[:crm_contact_ids].present?
+ end
+
+ def add_or_remove_present?
+ params[:add_crm_contact_ids].present? || params[:remove_crm_contact_ids].present?
+ end
+
+ def too_many?
+ params[:add_crm_contact_ids] && params[:add_crm_contact_ids].length > MAX_ADDITIONAL_CONTACTS
+ end
+
+ def error_no_permissions
+ ServiceResponse.error(message: ['You have insufficient permissions to set customer relations contacts for this issue'])
+ end
+
+ def error_invalid_params
+ ServiceResponse.error(message: ['You cannot combine crm_contact_ids with add_crm_contact_ids or remove_crm_contact_ids'])
+ end
+
+ def error_too_many
+ ServiceResponse.error(payload: issue, message: ["You can only add up to #{MAX_ADDITIONAL_CONTACTS} contacts at one time"])
+ end
+ end
+end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index d120b007af2..824a609dfb9 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -80,7 +80,7 @@ module Issues
todo_service.reassigned_assignable(issue, current_user, old_assignees)
track_incident_action(current_user, issue, :incident_assigned)
- if Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:broadcast_issue_updates, issue.project)
+ if Feature.enabled?(:broadcast_issue_updates, issue.project, default_enabled: :yaml)
GraphqlTriggers.issuable_assignees_updated(issue)
end
end
diff --git a/app/services/jira/requests/base.rb b/app/services/jira/requests/base.rb
index 56484075d08..a16f8bbd367 100644
--- a/app/services/jira/requests/base.rb
+++ b/app/services/jira/requests/base.rb
@@ -67,9 +67,19 @@ module Jira
ServiceResponse.error(message: error_message(e))
end
+ def auth_docs_link_start
+ auth_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira', anchor: 'authentication-in-jira')
+ '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auth_docs_link_url }
+ end
+
+ def config_docs_link_start
+ config_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira/configure')
+ '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: config_docs_link_url }
+ end
+
def error_message(error)
reportable_error_message(error) ||
- s_('JiraRequest|An error occurred while requesting data from Jira. Check your Jira integration configuration and try again.')
+ s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
end
# Returns a user-facing error message if possible, otherwise `nil`.
@@ -93,11 +103,11 @@ module Jira
def reportable_jira_ruby_error_message(error)
case error.message
when 'Unauthorized'
- s_('JiraRequest|The credentials for accessing Jira are not valid. Check your Jira integration credentials and try again.')
+ s_('JiraRequest|The credentials for accessing Jira are not valid. Check your %{docs_link_start}Jira integration credentials%{docs_link_end} and try again.').html_safe % { docs_link_start: auth_docs_link_start, docs_link_end: '</a>'.html_safe }
when 'Forbidden'
- s_('JiraRequest|The credentials for accessing Jira are not allowed to access the data. Check your Jira integration credentials and try again.')
+ s_('JiraRequest|The credentials for accessing Jira are not allowed to access the data. Check your %{docs_link_start}Jira integration credentials%{docs_link_end} and try again.').html_safe % { docs_link_start: auth_docs_link_start, docs_link_end: '</a>'.html_safe }
when 'Bad Request'
- s_('JiraRequest|An error occurred while requesting data from Jira. Check your Jira integration configuration and try again.')
+ s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
when /errorMessages/
jira_ruby_json_error_message(error.message)
end
@@ -111,7 +121,7 @@ module Jira
messages = Rails::Html::FullSanitizer.new.sanitize(messages).presence
return unless messages
- s_('JiraRequest|An error occurred while requesting data from Jira: %{messages}. Check your Jira integration configuration and try again.') % { messages: messages }
+ s_('JiraRequest|An error occurred while requesting data from Jira: %{messages}. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { messages: messages, docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
rescue JSON::ParserError
end
end
diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
index a05090d6bfb..19d419609a5 100644
--- a/app/services/labels/transfer_service.rb
+++ b/app/services/labels/transfer_service.rb
@@ -50,21 +50,32 @@ module Labels
# rubocop: disable CodeReuse/ActiveRecord
def group_labels_applied_to_issues
- @group_labels_applied_to_issues ||= Label.joins(:issues)
- .where(
- issues: { project_id: project.id },
- labels: { group_id: old_group.self_and_ancestors }
- )
+ @labels_applied_to_issues ||= if use_optimized_group_labels_query?
+ Label.joins(:issues)
+ .joins("INNER JOIN namespaces on namespaces.id = labels.group_id AND namespaces.type = 'Group'" )
+ .where(issues: { project_id: project.id }).reorder(nil)
+ else
+ Label.joins(:issues).where(
+ issues: { project_id: project.id },
+ labels: { group_id: old_group.self_and_ancestors }
+ )
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def group_labels_applied_to_merge_requests
- @group_labels_applied_to_merge_requests ||= Label.joins(:merge_requests)
- .where(
- merge_requests: { target_project_id: project.id },
- labels: { group_id: old_group.self_and_ancestors }
- )
+ @labels_applied_to_mrs ||= if use_optimized_group_labels_query?
+ Label.joins(:merge_requests)
+ .joins("INNER JOIN namespaces on namespaces.id = labels.group_id AND namespaces.type = 'Group'" )
+ .where(merge_requests: { target_project_id: project.id }).reorder(nil)
+ else
+ Label.joins(:merge_requests)
+ .where(
+ merge_requests: { target_project_id: project.id },
+ labels: { group_id: old_group.self_and_ancestors }
+ )
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -88,5 +99,9 @@ module Labels
.update_all(label_id: new_label_id)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def use_optimized_group_labels_query?
+ Feature.enabled?(:use_optimized_group_labels_query, project.root_namespace, default_enabled: :yaml)
+ end
end
end
diff --git a/app/services/loose_foreign_keys/batch_cleaner_service.rb b/app/services/loose_foreign_keys/batch_cleaner_service.rb
new file mode 100644
index 00000000000..06c05e8ff54
--- /dev/null
+++ b/app/services/loose_foreign_keys/batch_cleaner_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module LooseForeignKeys
+ class BatchCleanerService
+ def initialize(parent_klass:, deleted_parent_records:, modification_tracker: LooseForeignKeys::ModificationTracker.new, models_by_table_name:)
+ @parent_klass = parent_klass
+ @deleted_parent_records = deleted_parent_records
+ @modification_tracker = modification_tracker
+ @models_by_table_name = models_by_table_name
+ @deleted_records_counter = Gitlab::Metrics.counter(
+ :loose_foreign_key_processed_deleted_records,
+ 'The number of processed loose foreign key deleted records'
+ )
+ end
+
+ def execute
+ parent_klass.loose_foreign_key_definitions.each do |foreign_key_definition|
+ run_cleaner_service(foreign_key_definition, with_skip_locked: true)
+ break if modification_tracker.over_limit?
+
+ run_cleaner_service(foreign_key_definition, with_skip_locked: false)
+ break if modification_tracker.over_limit?
+ end
+
+ return if modification_tracker.over_limit?
+
+ # At this point, all associations are cleaned up, we can update the status of the parent records
+ update_count = LooseForeignKeys::DeletedRecord.mark_records_processed(deleted_parent_records)
+
+ deleted_records_counter.increment({ table: parent_klass.table_name, db_config_name: LooseForeignKeys::DeletedRecord.connection.pool.db_config.name }, update_count)
+ end
+
+ private
+
+ attr_reader :parent_klass, :deleted_parent_records, :modification_tracker, :models_by_table_name, :deleted_records_counter
+
+ def record_result(cleaner, result)
+ if cleaner.async_delete?
+ modification_tracker.add_deletions(result[:table], result[:affected_rows])
+ elsif cleaner.async_nullify?
+ modification_tracker.add_updates(result[:table], result[:affected_rows])
+ end
+ end
+
+ def run_cleaner_service(foreign_key_definition, with_skip_locked:)
+ cleaner = CleanerService.new(
+ model: models_by_table_name.fetch(foreign_key_definition.to_table),
+ foreign_key_definition: foreign_key_definition,
+ deleted_parent_records: deleted_parent_records,
+ with_skip_locked: with_skip_locked
+ )
+
+ loop do
+ result = cleaner.execute
+ record_result(cleaner, result)
+
+ break if modification_tracker.over_limit? || result[:affected_rows] == 0
+ end
+ end
+ end
+end
diff --git a/app/services/loose_foreign_keys/cleaner_service.rb b/app/services/loose_foreign_keys/cleaner_service.rb
new file mode 100644
index 00000000000..8fe053e2edf
--- /dev/null
+++ b/app/services/loose_foreign_keys/cleaner_service.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module LooseForeignKeys
+ # rubocop: disable CodeReuse/ActiveRecord
+ class CleanerService
+ DELETE_LIMIT = 1000
+ UPDATE_LIMIT = 500
+
+ delegate :connection, to: :model
+
+ def initialize(model:, foreign_key_definition:, deleted_parent_records:, with_skip_locked: false)
+ @model = model
+ @foreign_key_definition = foreign_key_definition
+ @deleted_parent_records = deleted_parent_records
+ @with_skip_locked = with_skip_locked
+ end
+
+ def execute
+ result = connection.execute(build_query)
+
+ { affected_rows: result.cmd_tuples, table: foreign_key_definition.to_table }
+ end
+
+ def async_delete?
+ foreign_key_definition.on_delete == :async_delete
+ end
+
+ def async_nullify?
+ foreign_key_definition.on_delete == :async_nullify
+ end
+
+ private
+
+ attr_reader :model, :foreign_key_definition, :deleted_parent_records, :with_skip_locked
+
+ def build_query
+ query = if async_delete?
+ delete_query
+ elsif async_nullify?
+ update_query
+ else
+ raise "Invalid on_delete argument: #{foreign_key_definition.on_delete}"
+ end
+
+ unless query.include?(%{"#{foreign_key_definition.column}" IN (})
+ raise("FATAL: foreign key condition is missing from the generated query: #{query}")
+ end
+
+ query
+ end
+
+ def arel_table
+ @arel_table ||= model.arel_table
+ end
+
+ def primary_keys
+ @primary_keys ||= connection.primary_keys(model.table_name).map { |key| arel_table[key] }
+ end
+
+ def quoted_table_name
+ @quoted_table_name ||= Arel.sql(connection.quote_table_name(model.table_name))
+ end
+
+ def delete_query
+ query = Arel::DeleteManager.new
+ query.from(quoted_table_name)
+
+ add_in_query_with_limit(query, DELETE_LIMIT)
+ end
+
+ def update_query
+ query = Arel::UpdateManager.new
+ query.table(quoted_table_name)
+ query.set([[arel_table[foreign_key_definition.column], nil]])
+
+ add_in_query_with_limit(query, UPDATE_LIMIT)
+ end
+
+ # IN query with one or composite primary key
+ # WHERE (primary_key1, primary_key2) IN (subselect)
+ def add_in_query_with_limit(query, limit)
+ columns = Arel::Nodes::Grouping.new(primary_keys)
+ query.where(columns.in(in_query_with_limit(limit))).to_sql
+ end
+
+ # Builds the following sub-query
+ # SELECT primary_keys FROM table WHERE foreign_key IN (1, 2, 3) LIMIT N
+ def in_query_with_limit(limit)
+ in_query = Arel::SelectManager.new
+ in_query.from(quoted_table_name)
+ in_query.where(arel_table[foreign_key_definition.column].in(deleted_parent_records.map(&:primary_key_value)))
+ in_query.projections = primary_keys
+ in_query.take(limit)
+ in_query.lock(Arel.sql('FOR UPDATE SKIP LOCKED')) if with_skip_locked
+ in_query
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
diff --git a/app/services/loose_foreign_keys/process_deleted_records_service.rb b/app/services/loose_foreign_keys/process_deleted_records_service.rb
new file mode 100644
index 00000000000..735fc8a2415
--- /dev/null
+++ b/app/services/loose_foreign_keys/process_deleted_records_service.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module LooseForeignKeys
+ class ProcessDeletedRecordsService
+ BATCH_SIZE = 1000
+
+ def initialize(connection:)
+ @connection = connection
+ end
+
+ def execute
+ modification_tracker = ModificationTracker.new
+
+ tracked_tables.cycle do |table|
+ records = load_batch_for_table(table)
+
+ if records.empty?
+ tracked_tables.delete(table)
+ next
+ end
+
+ break if modification_tracker.over_limit?
+
+ model = find_parent_model!(table)
+
+ LooseForeignKeys::BatchCleanerService
+ .new(parent_klass: model,
+ deleted_parent_records: records,
+ modification_tracker: modification_tracker,
+ models_by_table_name: models_by_table_name)
+ .execute
+
+ break if modification_tracker.over_limit?
+ end
+
+ modification_tracker.stats
+ end
+
+ private
+
+ attr_reader :connection
+
+ def load_batch_for_table(table)
+ fully_qualified_table_name = "#{current_schema}.#{table}"
+ LooseForeignKeys::DeletedRecord.load_batch_for_table(fully_qualified_table_name, BATCH_SIZE)
+ end
+
+ def find_parent_model!(table)
+ models_by_table_name.fetch(table)
+ end
+
+ def current_schema
+ @current_schema = connection.current_schema
+ end
+
+ def tracked_tables
+ @tracked_tables ||= models_by_table_name
+ .select { |table_name, model| model.respond_to?(:loose_foreign_key_definitions) }
+ .keys
+ end
+
+ def models_by_table_name
+ @models_by_table_name ||= begin
+ all_models
+ .select(&:base_class?)
+ .index_by(&:table_name)
+ end
+ end
+
+ def all_models
+ ApplicationRecord.descendants
+ end
+ end
+end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 0cc62e661a3..cb905e01613 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -63,10 +63,14 @@ module Members
invites,
params[:access_level],
expires_at: params[:expires_at],
- current_user: current_user
+ current_user: current_user,
+ tasks_to_be_done: params[:tasks_to_be_done],
+ tasks_project_id: params[:tasks_project_id]
)
members.each { |member| process_result(member) }
+
+ create_tasks_to_be_done
end
def process_result(member)
@@ -112,6 +116,19 @@ module Members
end
end
+ def create_tasks_to_be_done
+ return unless experiment(:invite_members_for_task).enabled?
+ return if params[:tasks_to_be_done].blank? || params[:tasks_project_id].blank?
+
+ valid_members = members.select { |member| member.valid? && member.member_task.valid? }
+ return unless valid_members.present?
+
+ # We can take the first `member_task` here, since all tasks will have the same attributes needed
+ # for the `TasksToBeDone::CreateWorker`, ie. `project` and `tasks_to_be_done`.
+ member_task = valid_members[0].member_task
+ TasksToBeDone::CreateWorker.perform_async(member_task.id, current_user.id, valid_members.map(&:user_id))
+ end
+
def areas_of_focus
params[:areas_of_focus] || []
end
diff --git a/app/services/members/creator_service.rb b/app/services/members/creator_service.rb
index 7b0bebff760..f2c8a6f20a1 100644
--- a/app/services/members/creator_service.rb
+++ b/app/services/members/creator_service.rb
@@ -4,6 +4,8 @@ module Members
# This class serves as more of an app-wide way we add/create members
# All roads to add members should take this path.
class CreatorService
+ include Gitlab::Experiment::Dsl
+
class << self
def parsed_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i }
@@ -24,6 +26,7 @@ module Members
def execute
find_or_build_member
update_member
+ create_member_task
member
end
@@ -61,6 +64,21 @@ module Members
}
end
+ def create_member_task
+ return unless experiment(:invite_members_for_task).enabled?
+ return unless member.persisted?
+ return if member_task_attributes.value?(nil)
+
+ member.create_member_task(member_task_attributes)
+ end
+
+ def member_task_attributes
+ {
+ tasks_to_be_done: args[:tasks_to_be_done],
+ project_id: args[:tasks_project_id]
+ }
+ end
+
def approve_request
::Members::ApproveAccessRequestService.new(current_user,
access_level: access_level)
diff --git a/app/services/members/invite_service.rb b/app/services/members/invite_service.rb
index 257a986b8dd..85acb720f0f 100644
--- a/app/services/members/invite_service.rb
+++ b/app/services/members/invite_service.rb
@@ -39,6 +39,11 @@ module Members
errors[invite_email(member)] = member.errors.full_messages.to_sentence
end
+ override :create_tasks_to_be_done
+ def create_tasks_to_be_done
+ # Only create task issues for existing users. Tasks for new users are created when they signup.
+ end
+
def invite_email(member)
member.invite_email || member.user.email
end
diff --git a/app/services/merge_requests/outdated_discussion_diff_lines_service.rb b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb
new file mode 100644
index 00000000000..a2de5a32963
--- /dev/null
+++ b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class OutdatedDiscussionDiffLinesService
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :project, :note
+
+ OVERFLOW_LINES_COUNT = 2
+
+ def initialize(project:, note:)
+ @project = project
+ @note = note
+ end
+
+ def execute
+ end_position = position.line_range["end"]
+ diff_line_index = diff_lines.find_index do |l|
+ if end_position["new_line"]
+ l.new_line == end_position["new_line"]
+ elsif end_position["old_line"]
+ l.old_line == end_position["old_line"]
+ end
+ end
+ initial_line_index = [diff_line_index - OVERFLOW_LINES_COUNT, 0].max
+ last_line_index = [diff_line_index + OVERFLOW_LINES_COUNT, diff_lines.length].min
+
+ prev_lines = []
+
+ diff_lines[initial_line_index..last_line_index].each do |line|
+ if line.meta?
+ prev_lines.clear
+ else
+ prev_lines << line
+ end
+ end
+
+ prev_lines
+ end
+
+ private
+
+ def position
+ note.change_position
+ end
+
+ def repository
+ project.repository
+ end
+
+ def diff_file
+ position.diff_file(repository)
+ end
+
+ def diff_lines
+ strong_memoize(:diff_lines) do
+ diff_file.highlighted_diff_lines
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/retarget_chain_service.rb b/app/services/merge_requests/retarget_chain_service.rb
index dab6e198979..33aae4184ae 100644
--- a/app/services/merge_requests/retarget_chain_service.rb
+++ b/app/services/merge_requests/retarget_chain_service.rb
@@ -5,8 +5,6 @@ module MergeRequests
MAX_RETARGET_MERGE_REQUESTS = 4
def execute(merge_request)
- return unless Feature.enabled?(:retarget_merge_requests, merge_request.target_project, default_enabled: :yaml)
-
# we can only retarget MRs that are targeting the same project
return unless merge_request.for_same_project? && merge_request.merged?
diff --git a/app/services/merge_requests/toggle_attention_requested_service.rb b/app/services/merge_requests/toggle_attention_requested_service.rb
new file mode 100644
index 00000000000..66c5d6fce5d
--- /dev/null
+++ b/app/services/merge_requests/toggle_attention_requested_service.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class ToggleAttentionRequestedService < MergeRequests::BaseService
+ attr_accessor :merge_request, :user
+
+ def initialize(project:, current_user:, merge_request:, user:)
+ super(project: project, current_user: current_user)
+
+ @merge_request = merge_request
+ @user = user
+ end
+
+ def execute
+ return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
+
+ if reviewer || assignee
+ update_state(reviewer)
+ update_state(assignee)
+
+ if reviewer&.attention_requested? || assignee&.attention_requested?
+ notity_user
+ end
+
+ success
+ else
+ error("User is not a reviewer or assignee of the merge request")
+ end
+ end
+
+ private
+
+ def notity_user
+ todo_service.create_attention_requested_todo(merge_request, current_user, user)
+ end
+
+ def assignee
+ merge_request.find_assignee(user)
+ end
+
+ def reviewer
+ merge_request.find_reviewer(user)
+ end
+
+ def update_state(reviewer_or_assignee)
+ reviewer_or_assignee&.update(state: reviewer_or_assignee&.attention_requested? ? :reviewed : :attention_requested)
+ end
+ end
+end
diff --git a/app/services/namespaces/in_product_marketing_email_records.rb b/app/services/namespaces/in_product_marketing_email_records.rb
new file mode 100644
index 00000000000..1237a05ea13
--- /dev/null
+++ b/app/services/namespaces/in_product_marketing_email_records.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Namespaces
+ class InProductMarketingEmailRecords
+ attr_reader :records
+
+ def initialize
+ @records = []
+ end
+
+ def save!
+ Users::InProductMarketingEmail.bulk_insert!(@records)
+ @records = []
+ end
+
+ def add(user, track, series)
+ @records << Users::InProductMarketingEmail.new(
+ user: user,
+ track: track,
+ series: series,
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now
+ )
+ end
+ end
+end
diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb
index 0401653cf3c..90900698e1a 100644
--- a/app/services/namespaces/in_product_marketing_emails_service.rb
+++ b/app/services/namespaces/in_product_marketing_emails_service.rb
@@ -56,7 +56,7 @@ module Namespaces
def initialize(track, interval)
@track = track
@interval = interval
- @in_product_marketing_email_records = []
+ @sent_email_records = InProductMarketingEmailRecords.new
end
def execute
@@ -71,17 +71,21 @@ module Namespaces
private
- attr_reader :track, :interval, :in_product_marketing_email_records
+ attr_reader :track, :interval, :sent_email_records
+
+ def send_email(user, group)
+ NotificationService.new.in_product_marketing(user.id, group.id, track, series)
+ end
def send_email_for_group(group)
users_for_group(group).each do |user|
if can_perform_action?(user, group)
send_email(user, group)
- track_sent_email(user, track, series)
+ sent_email_records.add(user, track, series)
end
end
- save_tracked_emails!
+ sent_email_records.save!
end
def groups_for_track
@@ -126,10 +130,6 @@ module Namespaces
end
end
- def send_email(user, group)
- NotificationService.new.in_product_marketing(user.id, group.id, track, series)
- end
-
def completed_actions
TRACKS[track][:completed_actions]
end
@@ -146,21 +146,6 @@ module Namespaces
def series
TRACKS[track][:interval_days].index(interval)
end
-
- def save_tracked_emails!
- Users::InProductMarketingEmail.bulk_insert!(in_product_marketing_email_records)
- @in_product_marketing_email_records = []
- end
-
- def track_sent_email(user, track, series)
- in_product_marketing_email_records << Users::InProductMarketingEmail.new(
- user: user,
- track: track,
- series: series,
- created_at: Time.zone.now,
- updated_at: Time.zone.now
- )
- end
end
end
diff --git a/app/services/namespaces/invite_team_email_service.rb b/app/services/namespaces/invite_team_email_service.rb
new file mode 100644
index 00000000000..45975d1953a
--- /dev/null
+++ b/app/services/namespaces/invite_team_email_service.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Namespaces
+ class InviteTeamEmailService
+ include Gitlab::Experiment::Dsl
+
+ TRACK = :invite_team
+ DELIVERY_DELAY_IN_MINUTES = 20.minutes
+
+ def self.send_email(user, group)
+ new(user, group).execute
+ end
+
+ def initialize(user, group)
+ @group = group
+ @user = user
+ @sent_email_records = InProductMarketingEmailRecords.new
+ end
+
+ def execute
+ return unless user.email_opted_in?
+ return unless group.root?
+ return unless group.setup_for_company
+
+ # Exclude group if users other than the creator have already been
+ # added/invited
+ return unless group.member_count == 1
+
+ return if email_for_track_sent_to_user?
+
+ experiment(:invite_team_email, group: group) do |e|
+ e.candidate do
+ send_email(user, group)
+ sent_email_records.add(user, track, series)
+ sent_email_records.save!
+ end
+
+ e.record!
+ end
+ end
+
+ private
+
+ attr_reader :user, :group, :sent_email_records
+
+ def send_email(user, group)
+ NotificationService.new.in_product_marketing(user.id, group.id, track, series)
+ end
+
+ def track
+ TRACK
+ end
+
+ def series
+ 0
+ end
+
+ def email_for_track_sent_to_user?
+ Users::InProductMarketingEmail.for_user_with_track_and_series(user, track, series).present?
+ end
+ end
+end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 194c3d7bf7b..9a0db3bb9aa 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -43,7 +43,7 @@ module Notes
private
def execute_quick_actions(note)
- return yield(false) unless quick_actions_service.supported?(note)
+ return yield(false) unless quick_actions_supported?(note)
content, update_params, message = quick_actions_service.execute(note, quick_action_options)
only_commands = content.empty?
@@ -54,6 +54,10 @@ module Notes
do_commands(note, update_params, message, only_commands)
end
+ def quick_actions_supported?(note)
+ quick_actions_service.supported?(note)
+ end
+
def quick_actions_service
@quick_actions_service ||= QuickActionsService.new(project, current_user)
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index afc9015e758..6ad3a74b85d 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -598,8 +598,8 @@ class NotificationService
user.notification_email_for(pipeline.project.group)
end
- if recipients.any?
- mailer.public_send(email_template, pipeline, recipients).deliver_later
+ recipients.each do |recipient|
+ mailer.public_send(email_template, pipeline, recipient).deliver_later
end
end
diff --git a/app/services/packages/create_dependency_service.rb b/app/services/packages/create_dependency_service.rb
index 2c80ec66dbc..10a86e44cb0 100644
--- a/app/services/packages/create_dependency_service.rb
+++ b/app/services/packages/create_dependency_service.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
module Packages
+ # rubocop: disable Gitlab/BulkInsert
class CreateDependencyService < BaseService
attr_reader :package, :dependencies
@@ -51,7 +52,7 @@ module Packages
}
end
- ids = database.bulk_insert(Packages::Dependency.table_name, rows, return_ids: true, on_conflict: :do_nothing)
+ ids = ApplicationRecord.legacy_bulk_insert(Packages::Dependency.table_name, rows, return_ids: true, on_conflict: :do_nothing)
return ids if ids.size == names_and_version_patterns.size
Packages::Dependency.uncached do
@@ -72,11 +73,8 @@ module Packages
}
end
- database.bulk_insert(Packages::DependencyLink.table_name, rows)
- end
-
- def database
- ::Gitlab::Database.main
+ ApplicationRecord.legacy_bulk_insert(Packages::DependencyLink.table_name, rows)
end
end
+ # rubocop: enable Gitlab/BulkInsert
end
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index 1d5d9c38432..ae9c92a3d3a 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -4,6 +4,8 @@ module Packages
class CreatePackageService < ::Packages::CreatePackageService
include Gitlab::Utils::StrongMemoize
+ PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename].freeze
+
def execute
return error('Version is empty.', 400) if version.blank?
return error('Package already exists.', 403) if current_package_exists?
@@ -21,6 +23,10 @@ module Packages
::Packages::CreateDependencyService.new(package, package_dependencies).execute
::Packages::Npm::CreateTagService.new(package, dist_tag).execute
+ if Feature.enabled?(:packages_npm_abbreviated_metadata, project, default_enabled: :yaml)
+ package.create_npm_metadatum!(package_json: package_json)
+ end
+
package
end
@@ -46,6 +52,10 @@ module Packages
params[:versions][version]
end
+ def package_json
+ version_data.except(*PACKAGE_JSON_NOT_ALLOWED_FIELDS)
+ end
+
def dist_tag
params['dist-tags'].each_key.first
end
diff --git a/app/services/packages/nuget/create_dependency_service.rb b/app/services/packages/nuget/create_dependency_service.rb
index 3fc42056d43..85f295ac7b7 100644
--- a/app/services/packages/nuget/create_dependency_service.rb
+++ b/app/services/packages/nuget/create_dependency_service.rb
@@ -41,7 +41,7 @@ module Packages
}
end
- ::Gitlab::Database.main.bulk_insert(::Packages::Nuget::DependencyLinkMetadatum.table_name, rows.compact) # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert(::Packages::Nuget::DependencyLinkMetadatum.table_name, rows.compact) # rubocop:disable Gitlab/BulkInsert
end
def raw_dependency_for(dependency)
diff --git a/app/services/packages/rubygems/create_dependencies_service.rb b/app/services/packages/rubygems/create_dependencies_service.rb
index dea429148cf..0b2ae56bf45 100644
--- a/app/services/packages/rubygems/create_dependencies_service.rb
+++ b/app/services/packages/rubygems/create_dependencies_service.rb
@@ -3,8 +3,6 @@
module Packages
module Rubygems
class CreateDependenciesService
- include BulkInsertSafe
-
def initialize(package, gemspec)
@package = package
@gemspec = gemspec
diff --git a/app/services/packages/update_tags_service.rb b/app/services/packages/update_tags_service.rb
index 2bdf75a6617..f29c54dacb9 100644
--- a/app/services/packages/update_tags_service.rb
+++ b/app/services/packages/update_tags_service.rb
@@ -15,7 +15,7 @@ module Packages
tags_to_create = @tags - existing_tags
@package.tags.with_name(tags_to_destroy).delete_all if tags_to_destroy.any?
- ::Gitlab::Database.main.bulk_insert(Packages::Tag.table_name, rows(tags_to_create)) if tags_to_create.any? # rubocop:disable Gitlab/BulkInsert
+ ::ApplicationRecord.legacy_bulk_insert(Packages::Tag.table_name, rows(tags_to_create)) if tags_to_create.any? # rubocop:disable Gitlab/BulkInsert
end
private
diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb
index a5ee7173bdf..e5d40b60747 100644
--- a/app/services/projects/alerting/notify_service.rb
+++ b/app/services/projects/alerting/notify_service.rb
@@ -5,6 +5,7 @@ module Projects
class NotifyService
extend ::Gitlab::Utils::Override
include ::AlertManagement::AlertProcessing
+ include ::AlertManagement::Responses
def initialize(project, payload)
@project = project
@@ -23,7 +24,7 @@ module Projects
complete_post_processing_tasks
- ServiceResponse.success
+ success(alert)
end
private
@@ -46,18 +47,6 @@ module Projects
def valid_token?(token)
token == integration.token
end
-
- def bad_request
- ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
- end
-
- def unauthorized
- ServiceResponse.error(message: 'Unauthorized', http_status: :unauthorized)
- end
-
- def forbidden
- ServiceResponse.error(message: 'Forbidden', http_status: :forbidden)
- end
end
end
end
diff --git a/app/services/projects/all_issues_count_service.rb b/app/services/projects/all_issues_count_service.rb
new file mode 100644
index 00000000000..15352b14d3e
--- /dev/null
+++ b/app/services/projects/all_issues_count_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Projects
+ # Service class for counting and caching the number of all issues of a
+ # project.
+ class AllIssuesCountService < Projects::CountService
+ def relation_for_count
+ @project.issues
+ end
+
+ def cache_key_name
+ 'all_issues_count'
+ end
+ end
+end
diff --git a/app/services/projects/all_merge_requests_count_service.rb b/app/services/projects/all_merge_requests_count_service.rb
new file mode 100644
index 00000000000..db0bab3f799
--- /dev/null
+++ b/app/services/projects/all_merge_requests_count_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Projects
+ # Service class for counting and caching the number of all merge requests of
+ # a project.
+ class AllMergeRequestsCountService < Projects::CountService
+ def relation_for_count
+ @project.merge_requests
+ end
+
+ def cache_key_name
+ 'all_merge_requests_count'
+ end
+ end
+end
diff --git a/app/services/projects/container_repository/cache_tags_created_at_service.rb b/app/services/projects/container_repository/cache_tags_created_at_service.rb
deleted file mode 100644
index 3a5346d7a23..00000000000
--- a/app/services/projects/container_repository/cache_tags_created_at_service.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module ContainerRepository
- class CacheTagsCreatedAtService
- def initialize(container_repository)
- @container_repository = container_repository
- @cached_tag_names = Set.new
- end
-
- def populate(tags)
- return if tags.empty?
-
- # This will load all tags in one Redis roundtrip
- # the maximum number of tags is configurable and is set to 200 by default.
- # https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/packages/container_registry/index.md#set-cleanup-limits-to-conserve-resources
- keys = tags.map(&method(:cache_key))
- cached_tags_count = 0
-
- ::Gitlab::Redis::Cache.with do |redis|
- tags.zip(redis.mget(keys)).each do |tag, created_at|
- next unless created_at
-
- tag.created_at = DateTime.rfc3339(created_at)
- @cached_tag_names << tag.name
- cached_tags_count += 1
- end
- end
-
- cached_tags_count
- end
-
- def insert(tags, max_ttl_in_seconds)
- return unless max_ttl_in_seconds
- return if tags.empty?
-
- # tags with nil created_at are not cacheable
- # tags already cached don't need to be cached again
- cacheable_tags = tags.select do |tag|
- tag.created_at.present? && !tag.name.in?(@cached_tag_names)
- end
-
- return if cacheable_tags.empty?
-
- now = Time.zone.now
-
- ::Gitlab::Redis::Cache.with do |redis|
- # we use a pipeline instead of a MSET because each tag has
- # a specific ttl
- redis.pipelined do
- cacheable_tags.each do |tag|
- created_at = tag.created_at
- # ttl is the max_ttl_in_seconds reduced by the number
- # of seconds that the tag has already existed
- ttl = max_ttl_in_seconds - (now - created_at).seconds
- ttl = ttl.to_i
- redis.set(cache_key(tag), created_at.rfc3339, ex: ttl) if ttl > 0
- end
- end
- end
- end
-
- private
-
- def cache_key(tag)
- "container_repository:{#{@container_repository.id}}:tag:#{tag.name}:created_at"
- end
- end
- end
-end
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
index 3a60de0f1ee..1a788abac12 100644
--- a/app/services/projects/container_repository/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -140,14 +140,13 @@ module Projects
def cache
strong_memoize(:cache) do
- ::Projects::ContainerRepository::CacheTagsCreatedAtService.new(@container_repository)
+ ::Gitlab::ContainerRepository::Tags::Cache.new(@container_repository)
end
end
def caching_enabled?
container_expiration_policy &&
- older_than.present? &&
- Feature.enabled?(:container_registry_expiration_policies_caching, @project)
+ older_than.present?
end
def throttling_enabled?
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 1536f0a22b8..1d187b140ef 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -45,7 +45,7 @@ module Projects
if namespace_id
# Find matching namespace and check if it allowed
# for current user if namespace_id passed.
- unless current_user.can?(:create_projects, project_namespace)
+ unless current_user.can?(:create_projects, parent_namespace)
@project.namespace_id = nil
deny_namespace
return @project
@@ -136,7 +136,7 @@ module Projects
access_level: group_access_level)
end
- AuthorizedProjectUpdate::ProjectCreateWorker.perform_async(@project.id)
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(@project.id)
# AuthorizedProjectsWorker uses an exclusive lease per user but
# specialized workers might have synchronization issues. Until we
# compare the inconsistency rates of both approaches, we still run
@@ -227,14 +227,14 @@ module Projects
def extra_attributes_for_measurement
{
current_user: current_user&.name,
- project_full_path: "#{project_namespace&.full_path}/#{@params[:path]}"
+ project_full_path: "#{parent_namespace&.full_path}/#{@params[:path]}"
}
end
private
- def project_namespace
- @project_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace
+ def parent_namespace
+ @parent_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace
end
def create_from_template?
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 27f813f4661..b7ed9202b01 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -152,9 +152,12 @@ module Projects
deleted_count = project.commit_statuses.delete_all
- if deleted_count > 0
- Gitlab::AppLogger.info "Projects::DestroyService - Project #{project.id} - #{deleted_count} leftover commit statuses"
- end
+ Gitlab::AppLogger.info(
+ class: 'Projects::DestroyService',
+ project_id: project.id,
+ message: 'leftover commit statuses',
+ orphaned_commit_status_count: deleted_count
+ )
end
# The project can have multiple webhooks with hundreds of thousands of web_hook_logs.
diff --git a/app/services/projects/detect_repository_languages_service.rb b/app/services/projects/detect_repository_languages_service.rb
index 0356a6b0ccd..9db0b71d106 100644
--- a/app/services/projects/detect_repository_languages_service.rb
+++ b/app/services/projects/detect_repository_languages_service.rb
@@ -21,7 +21,7 @@ module Projects
.update_all(share: update[:share])
end
- Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert( # rubocop:disable Gitlab/BulkInsert
RepositoryLanguage.table_name,
detection.insertions(matching_programming_languages)
)
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 64c0f1ff4ac..b1a2182fbdc 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -36,6 +36,7 @@ module Projects
private
attr_accessor :shared
+ attr_reader :logger
def execute_after_export_action(after_export_strategy)
return unless after_export_strategy
@@ -74,7 +75,11 @@ module Projects
end
def project_tree_saver
- tree_saver_class.new(project: project, current_user: current_user, shared: shared, params: params)
+ tree_saver_class.new(project: project,
+ current_user: current_user,
+ shared: shared,
+ params: params,
+ logger: logger)
end
def tree_saver_class
@@ -116,7 +121,7 @@ module Projects
end
def notify_success
- @logger.info(
+ logger.info(
message: 'Project successfully exported',
project_name: project.name,
project_id: project.id
@@ -124,7 +129,7 @@ module Projects
end
def notify_error
- @logger.error(
+ logger.error(
message: 'Project export error',
export_errors: shared.errors.join(', '),
project_name: project.name,
diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb
index 7c00b9e6105..cf3cc5cd8e0 100644
--- a/app/services/projects/lfs_pointers/lfs_link_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_link_service.rb
@@ -38,7 +38,7 @@ module Projects
rows = existent_lfs_objects
.not_linked_to_project(project)
.map { |existing_lfs_object| { project_id: project.id, lfs_object_id: existing_lfs_object.id } }
- Gitlab::Database.main.bulk_insert(:lfs_objects_projects, rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(:lfs_objects_projects, rows) # rubocop:disable Gitlab/BulkInsert
iterations += 1
linked_existing_objects += existent_lfs_objects.map(&:oid)
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index 1616a8a4062..152590fffff 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -36,17 +36,9 @@ module Projects
private
def project_members_through_invited_groups
- groups_with_ancestors = if ::Feature.enabled?(:linear_participants_service_ancestor_scopes, current_user, default_enabled: :yaml)
- visible_groups.self_and_ancestors
- else
- Gitlab::ObjectHierarchy
- .new(visible_groups)
- .base_and_ancestors
- end
-
GroupMember
.active_without_invites_and_requests
- .with_source_id(groups_with_ancestors.pluck_primary_key)
+ .with_source_id(visible_groups.self_and_ancestors.pluck_primary_key)
end
def visible_groups
diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb
index c1bf2e68436..56f65718d24 100644
--- a/app/services/projects/prometheus/alerts/notify_service.rb
+++ b/app/services/projects/prometheus/alerts/notify_service.rb
@@ -6,6 +6,7 @@ module Projects
class NotifyService
include Gitlab::Utils::StrongMemoize
include ::IncidentManagement::Settings
+ include ::AlertManagement::Responses
# This set of keys identifies a payload as a valid Prometheus
# payload and thus processable by this service. See also
@@ -27,9 +28,9 @@ module Projects
return unprocessable_entity unless self.class.processable?(payload)
return unauthorized unless valid_alert_manager_token?(token, integration)
- process_prometheus_alerts
+ alert_responses = process_prometheus_alerts
- ServiceResponse.success
+ alert_response(alert_responses)
end
def self.processable?(payload)
@@ -128,23 +129,17 @@ module Projects
end
def process_prometheus_alerts
- alerts.each do |alert|
+ alerts.map do |alert|
AlertManagement::ProcessPrometheusAlertService
.new(project, alert.to_h)
.execute
end
end
- def bad_request
- ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
- end
-
- def unauthorized
- ServiceResponse.error(message: 'Unauthorized', http_status: :unauthorized)
- end
+ def alert_response(alert_responses)
+ alerts = alert_responses.map { |resp| resp.payload[:alert] }.compact
- def unprocessable_entity
- ServiceResponse.error(message: 'Unprocessable Entity', http_status: :unprocessable_entity)
+ success(alerts)
end
end
end
diff --git a/app/services/resource_events/base_synthetic_notes_builder_service.rb b/app/services/resource_events/base_synthetic_notes_builder_service.rb
index 5939b9d2f9c..192d40129a3 100644
--- a/app/services/resource_events/base_synthetic_notes_builder_service.rb
+++ b/app/services/resource_events/base_synthetic_notes_builder_service.rb
@@ -24,10 +24,18 @@ module ResourceEvents
private
def apply_common_filters(events)
+ events = apply_pagination(events)
events = apply_last_fetched_at(events)
apply_fetch_until(events)
end
+ def apply_pagination(events)
+ return events if params[:paginated_notes].nil?
+ return events.none if params[:paginated_notes][table_name].blank?
+
+ events.id_in(params[:paginated_notes][table_name].map(&:id))
+ end
+
def apply_last_fetched_at(events)
return events unless params[:last_fetched_at].present?
@@ -47,5 +55,9 @@ module ResourceEvents
resource.project || resource.group
end
end
+
+ def table_name
+ raise NotImplementedError
+ end
end
end
diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb
index bc2d3a946cc..03ac839c509 100644
--- a/app/services/resource_events/change_labels_service.rb
+++ b/app/services/resource_events/change_labels_service.rb
@@ -23,7 +23,7 @@ module ResourceEvents
label_hash.merge(label_id: label.id, action: ResourceLabelEvent.actions['remove'])
end
- Gitlab::Database.main.bulk_insert(ResourceLabelEvent.table_name, labels) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert(ResourceLabelEvent.table_name, labels) # rubocop:disable Gitlab/BulkInsert
resource.expire_note_etag_cache
Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_label_changed_action(author: user) if resource.is_a?(Issue)
diff --git a/app/services/resource_events/synthetic_label_notes_builder_service.rb b/app/services/resource_events/synthetic_label_notes_builder_service.rb
index 5915ea938cf..0e5d945d13c 100644
--- a/app/services/resource_events/synthetic_label_notes_builder_service.rb
+++ b/app/services/resource_events/synthetic_label_notes_builder_service.rb
@@ -23,5 +23,9 @@ module ResourceEvents
events.group_by { |event| event.discussion_id }
end
+
+ def table_name
+ 'resource_label_events'
+ end
end
end
diff --git a/app/services/resource_events/synthetic_milestone_notes_builder_service.rb b/app/services/resource_events/synthetic_milestone_notes_builder_service.rb
index 10acf94e22b..0e2b171e192 100644
--- a/app/services/resource_events/synthetic_milestone_notes_builder_service.rb
+++ b/app/services/resource_events/synthetic_milestone_notes_builder_service.rb
@@ -21,5 +21,9 @@ module ResourceEvents
events = resource.resource_milestone_events.includes(user: :status) # rubocop: disable CodeReuse/ActiveRecord
apply_common_filters(events)
end
+
+ def table_name
+ 'resource_milestone_events'
+ end
end
end
diff --git a/app/services/resource_events/synthetic_state_notes_builder_service.rb b/app/services/resource_events/synthetic_state_notes_builder_service.rb
index 71d40200365..e17882b00de 100644
--- a/app/services/resource_events/synthetic_state_notes_builder_service.rb
+++ b/app/services/resource_events/synthetic_state_notes_builder_service.rb
@@ -16,5 +16,9 @@ module ResourceEvents
events = resource.resource_state_events.includes(user: :status) # rubocop: disable CodeReuse/ActiveRecord
apply_common_filters(events)
end
+
+ def table_name
+ 'resource_state_events'
+ end
end
end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index cce7821a226..4ba1b3ade86 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -75,6 +75,10 @@ class SearchService
search_results.highlight_map(scope)
end
+ def search_aggregations
+ search_results.aggregations(scope)
+ end
+
private
def page
diff --git a/app/services/security/ci_configuration/sast_iac_create_service.rb b/app/services/security/ci_configuration/sast_iac_create_service.rb
new file mode 100644
index 00000000000..80e9cf963da
--- /dev/null
+++ b/app/services/security/ci_configuration/sast_iac_create_service.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Security
+ module CiConfiguration
+ class SastIacCreateService < ::Security::CiConfiguration::BaseCreateService
+ private
+
+ def action
+ Security::CiConfiguration::SastIacBuildAction.new(project.auto_devops_enabled?, existing_gitlab_ci_content).generate
+ end
+
+ def next_branch
+ 'set-sast-iac-config'
+ end
+
+ def message
+ _('Configure SAST IaC in `.gitlab-ci.yml`, creating this file if it does not already exist')
+ end
+
+ def description
+ _('Configure SAST IaC in `.gitlab-ci.yml` using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST IaC settings.')
+ end
+ end
+ end
+end
diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb
index d83b21271c0..76d5063c337 100644
--- a/app/services/snippets/update_service.rb
+++ b/app/services/snippets/update_service.rb
@@ -148,3 +148,5 @@ module Snippets
end
end
end
+
+Snippets::UpdateService.prepend_mod_with('Snippets::UpdateService')
diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb
index 8d995631db6..c8bdcf4310b 100644
--- a/app/services/spam/spam_verdict_service.rb
+++ b/app/services/spam/spam_verdict_service.rb
@@ -73,18 +73,12 @@ module Spam
begin
result, attribs, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context)
- return [nil, attribs] unless result
-
# @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545
- return [result, attribs] if result == NOOP || attribs["monitorMode"] == "true"
+ return [nil, attribs] unless result
+
+ [result, attribs]
- # Duplicate logic with Akismet logic in #akismet_verdict
- if Gitlab::Recaptcha.enabled? && result != ALLOW
- [CONDITIONAL_ALLOW, attribs]
- else
- [result, attribs]
- end
rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e)
diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb
index eb98ed57d55..239cd86e0ec 100644
--- a/app/services/suggestions/create_service.rb
+++ b/app/services/suggestions/create_service.rb
@@ -25,7 +25,7 @@ module Suggestions
end
rows.in_groups_of(100, false) do |rows|
- Gitlab::Database.main.bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert
+ ApplicationRecord.legacy_bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert
end
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter.track_add_suggestion_action(note: @note)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index e5080718b69..dc5cf0fe554 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -327,6 +327,10 @@ module SystemNoteService
::SystemNotes::IncidentService.new(noteable: incident, project: incident.project, author: author).change_incident_severity
end
+ def resolve_incident_status(incident, author)
+ ::SystemNotes::IncidentService.new(noteable: incident, project: incident.project, author: author).resolve_incident_status
+ end
+
def log_resolving_alert(alert, monitoring_tool)
::SystemNotes::AlertManagementService.new(noteable: alert, project: alert.project).log_resolving_alert(monitoring_tool)
end
diff --git a/app/services/system_notes/incident_service.rb b/app/services/system_notes/incident_service.rb
index 4628662f0e9..785291e0637 100644
--- a/app/services/system_notes/incident_service.rb
+++ b/app/services/system_notes/incident_service.rb
@@ -25,5 +25,11 @@ module SystemNotes
)
end
end
+
+ def resolve_incident_status
+ body = 'changed the status to **Resolved** by closing the incident'
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'status'))
+ end
end
end
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 62aead352aa..94629ae7609 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -176,7 +176,13 @@ module SystemNotes
body = cross_reference_note_content(gfm_reference)
if noteable.is_a?(ExternalIssue)
- noteable.project.external_issue_tracker.create_cross_reference_note(noteable, mentioner, author)
+ Integrations::CreateExternalCrossReferenceWorker.perform_async(
+ noteable.project_id,
+ noteable.id,
+ mentioner.class.name,
+ mentioner.id,
+ author.id
+ )
else
track_cross_reference_action
create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))
diff --git a/app/services/tasks_to_be_done/base_service.rb b/app/services/tasks_to_be_done/base_service.rb
new file mode 100644
index 00000000000..a5648ad10c4
--- /dev/null
+++ b/app/services/tasks_to_be_done/base_service.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module TasksToBeDone
+ class BaseService < ::IssuableBaseService
+ LABEL_PREFIX = 'tasks to be done'
+
+ def initialize(project:, current_user:, assignee_ids: [])
+ params = {
+ assignee_ids: assignee_ids,
+ title: title,
+ description: description,
+ add_labels: label_name
+ }
+ super(project: project, current_user: current_user, params: params)
+ end
+
+ def execute
+ if (issue = existing_task_issue)
+ update_service = Issues::UpdateService.new(project: project, current_user: current_user, params: { add_assignee_ids: params[:assignee_ids] })
+ update_service.execute(issue)
+ else
+ build_service = Issues::BuildService.new(project: project, current_user: current_user, params: params)
+ create(build_service.execute)
+ end
+ end
+
+ private
+
+ def existing_task_issue
+ IssuesFinder.new(
+ current_user,
+ project_id: project.id,
+ state: 'opened',
+ non_archived: true,
+ label_name: label_name
+ ).execute.last
+ end
+
+ def title
+ raise NotImplementedError
+ end
+
+ def description
+ raise NotImplementedError
+ end
+
+ def label_suffix
+ raise NotImplementedError
+ end
+
+ def label_name
+ "#{LABEL_PREFIX}:#{label_suffix}"
+ end
+ end
+end
diff --git a/app/services/tasks_to_be_done/create_ci_task_service.rb b/app/services/tasks_to_be_done/create_ci_task_service.rb
new file mode 100644
index 00000000000..025ca2feb8e
--- /dev/null
+++ b/app/services/tasks_to_be_done/create_ci_task_service.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module TasksToBeDone
+ class CreateCiTaskService < BaseService
+ protected
+
+ def title
+ 'Set up CI/CD'
+ end
+
+ def description
+ <<~DESCRIPTION
+ GitLab CI/CD is a tool built into GitLab for software development through the [continuous methodologies](https://docs.gitlab.com/ee/ci/introduction/index.html#introduction-to-cicd-methodologies):
+
+ * Continuous Integration (CI)
+ * Continuous Delivery (CD)
+ * Continuous Deployment (CD)
+
+ Continuous Integration works by pushing small changes to your application’s codebase hosted in a Git repository, and, to every push, run a pipeline of scripts to build, test, and validate the code changes before merging them into the main branch.
+
+ Continuous Delivery and Deployment consist of a step further CI, deploying your application to production at every push to the default branch of the repository.
+
+ These methodologies allow you to catch bugs and errors early in the development cycle, ensuring that all the code deployed to production complies with the code standards you established for your app.
+
+ * :book: [Read the documentation](https://docs.gitlab.com/ee/ci/introduction/index.html)
+ * :clapper: [Watch a Demo](https://www.youtube.com/watch?v=1iXFbchozdY)
+
+ ## Next steps
+
+ * [ ] To start we recommend reviewing the following documentation:
+ * [ ] [How GitLab CI/CD works.](https://docs.gitlab.com/ee/ci/introduction/index.html#how-gitlab-cicd-works)
+ * [ ] [Fundamental pipeline architectures.](https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html)
+ * [ ] [GitLab CI/CD basic workflow.](https://docs.gitlab.com/ee/ci/introduction/index.html#basic-cicd-workflow)
+ * [ ] [Step-by-step guide for writing .gitlab-ci.yml for the first time.](https://docs.gitlab.com/ee/user/project/pages/getting_started_part_four.html)
+ * [ ] When you're ready select **Projects** (in the top navigation bar) > **Your projects** > select the Project you've already created.
+ * [ ] Select **CI / CD** in the left navigation to start setting up CI / CD in your project.
+ DESCRIPTION
+ end
+
+ def label_suffix
+ 'ci'
+ end
+ end
+end
diff --git a/app/services/tasks_to_be_done/create_code_task_service.rb b/app/services/tasks_to_be_done/create_code_task_service.rb
new file mode 100644
index 00000000000..dc3b9366a66
--- /dev/null
+++ b/app/services/tasks_to_be_done/create_code_task_service.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module TasksToBeDone
+ class CreateCodeTaskService < BaseService
+ protected
+
+ def title
+ 'Create or import your code into your Project (Repository)'
+ end
+
+ def description
+ <<~DESCRIPTION
+ You've already created your Group and Project within GitLab; we'll quickly review this hierarchy below. Once you're within your project you can easily create or import repositories.
+
+ **With GitLab Groups, you can:**
+
+ * Create one or multiple Projects for hosting your codebase (repositories).
+ * Assemble related projects together.
+ * Grant members access to several projects at once.
+
+ Groups can also be nested in subgroups.
+
+ Read more about groups in our [documentation](https://docs.gitlab.com/ee/user/group/).
+
+ **Within GitLab Projects, you can**
+
+ * Use it as an issue tracker.
+ * Collaborate on code.
+ * Continuously build, test, and deploy your app with built-in GitLab CI/CD.
+
+ You can also import an existing repository by providing the Git URL.
+
+ * :book: [Read the documentation](https://docs.gitlab.com/ee/user/project/index.html).
+
+ ## Next steps
+
+ Create or import your first repository into the project you created:
+
+ * [ ] Click **Projects** in the top navigation bar, then click **Your projects**.
+ * [ ] Select the Project that you created, then select **Repository**.
+ * [ ] Once on the Repository page you can select the **+** icon to add or import files.
+ * [ ] You can review our full documentation on creating [repositories](https://docs.gitlab.com/ee/user/project/repository/) in GitLab.
+
+ :tada: All done, you can close this issue!
+ DESCRIPTION
+ end
+
+ def label_suffix
+ 'code'
+ end
+ end
+end
diff --git a/app/services/tasks_to_be_done/create_issues_task_service.rb b/app/services/tasks_to_be_done/create_issues_task_service.rb
new file mode 100644
index 00000000000..a2de6852868
--- /dev/null
+++ b/app/services/tasks_to_be_done/create_issues_task_service.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module TasksToBeDone
+ class CreateIssuesTaskService < BaseService
+ protected
+
+ def title
+ 'Create/import issues (tickets) to collaborate on ideas and plan work'
+ end
+
+ def description
+ <<~DESCRIPTION
+ Issues allow you and your team to discuss proposals before, and during, their implementation. They can be used for a variety of other purposes, customized to your needs and workflow.
+
+ Issues are always associated with a specific project. If you have multiple projects in a group, you can view all the issues at the group level. [You can review our full Issue documentation here.](https://docs.gitlab.com/ee/user/project/issues/)
+
+ If you have existing issues or equivalent tickets you can import them as long as they are formatted as a CSV file, [the import process is covered here](https://docs.gitlab.com/ee/user/project/issues/csv_import.html).
+
+ **Common use cases include:**
+
+ * Discussing the implementation of a new idea
+ * Tracking tasks and work status
+ * Accepting feature proposals, questions, support requests, or bug reports
+ * Elaborating on new code implementations
+
+ ## Next steps
+
+ * [ ] Select **Projects** in the top navigation > **Your Projects** > select the Project you've already created.
+ * [ ] Once you've selected that project, you can select **Issues** in the left navigation, then click **New issue**.
+ * [ ] Fill in the title and description in the **New issue** page.
+ * [ ] Click on **Create issue**.
+
+ Pro tip: When you're in a group or project you can always utilize the **+** icon in the top navigation (located to the left of the search bar) to quickly create new issues.
+
+ That's it! You can close this issue.
+ DESCRIPTION
+ end
+
+ def label_suffix
+ 'issues'
+ end
+ end
+end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 71bb813f384..091f441831a 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -217,6 +217,11 @@ class TodoService
create_todos(reviewers, attributes)
end
+ def create_attention_requested_todo(target, author, users)
+ attributes = attributes_for_todo(target.project, target, author, Todo::ATTENTION_REQUESTED)
+ create_todos(users, attributes)
+ end
+
private
def create_todos(users, attributes)
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 4ec875098fa..1634cc017ae 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -65,7 +65,10 @@ module Users
user.destroy_dependent_associations_in_batches(exclude: [:snippets])
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
- user_data = user.destroy
+ user_data = nil
+ ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/340260') do
+ user_data = user.destroy
+ end
namespace.destroy
user_data
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 1850fa9747d..2d9766c3c56 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -67,10 +67,8 @@ module Users
def update_authorizations(remove = [], add = [])
log_refresh_details(remove, add)
- User.transaction do
- user.remove_project_authorizations(remove) unless remove.empty?
- ProjectAuthorization.insert_authorizations(add) unless add.empty?
- end
+ user.remove_project_authorizations(remove) unless remove.empty?
+ ProjectAuthorization.insert_authorizations(add) unless add.empty?
# Since we batch insert authorization rows, Rails' associations may get
# out of sync. As such we force a reload of the User object.
diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb
index 86b5b923418..61cf598f178 100644
--- a/app/services/users/upsert_credit_card_validation_service.rb
+++ b/app/services/users/upsert_credit_card_validation_service.rb
@@ -12,6 +12,7 @@ module Users
credit_card_validated_at: params.fetch(:credit_card_validated_at),
expiration_date: get_expiration_date(params),
last_digits: Integer(params.fetch(:credit_card_mask_number), 10),
+ network: params.fetch(:credit_card_type),
holder_name: params.fetch(:credit_card_holder_name)
}