diff options
Diffstat (limited to 'app/services')
82 files changed, 1024 insertions, 496 deletions
diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb index 6a33ec071db..7c0e9228b28 100644 --- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb +++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb @@ -11,7 +11,7 @@ module AutoMerge end def process(merge_request) - return unless merge_request.actual_head_pipeline&.success? + return unless merge_request.actual_head_pipeline_success? return unless merge_request.mergeable? merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params) diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb index 82cba1b68c4..c96ea970943 100644 --- a/app/services/boards/lists/list_service.rb +++ b/app/services/boards/lists/list_service.rb @@ -6,7 +6,7 @@ module Boards def execute(board) board.lists.create(list_type: :backlog) unless board.lists.backlog.exists? - board.lists.preload_associations + board.lists.preload_associated_models end end end diff --git a/app/services/branches/create_service.rb b/app/services/branches/create_service.rb new file mode 100644 index 00000000000..c8afd97e6bf --- /dev/null +++ b/app/services/branches/create_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Branches + class CreateService < BaseService + def execute(branch_name, ref, create_master_if_empty: true) + create_master_branch if create_master_if_empty && project.empty_repo? + + result = ::Branches::ValidateNewService.new(project).execute(branch_name) + + return result if result[:status] == :error + + new_branch = repository.add_branch(current_user, branch_name, ref) + + if new_branch + success(new_branch) + else + error("Invalid reference name: #{branch_name}") + end + rescue Gitlab::Git::PreReceiveError => ex + error(ex.message) + end + + def success(branch) + super().merge(branch: branch) + end + + private + + def create_master_branch + project.repository.create_file( + current_user, + '/README.md', + '', + message: 'Add README.md', + branch_name: 'master' + ) + end + end +end diff --git a/app/services/branches/delete_merged_service.rb b/app/services/branches/delete_merged_service.rb new file mode 100644 index 00000000000..9fd5964bf94 --- /dev/null +++ b/app/services/branches/delete_merged_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Branches + class DeleteMergedService < BaseService + def async_execute + DeleteMergedBranchesWorker.perform_async(project.id, current_user.id) + end + + def execute + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project) + + branches = project.repository.merged_branch_names + # Prevent deletion of branches relevant to open merge requests + branches -= merge_request_branch_names + # Prevent deletion of protected branches + branches = branches.reject { |branch| ProtectedBranch.protected?(project, branch) } + + branches.each do |branch| + ::Branches::DeleteService.new(project, current_user).execute(branch) + end + end + + private + + # rubocop: disable CodeReuse/ActiveRecord + def merge_request_branch_names + # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY + source_names = project.origin_merge_requests.opened.reorder(nil).distinct.pluck(:source_branch) + target_names = project.merge_requests.opened.reorder(nil).distinct.pluck(:target_branch) + (source_names + target_names).uniq + end + # rubocop: enable CodeReuse/ActiveRecord + end +end diff --git a/app/services/branches/delete_service.rb b/app/services/branches/delete_service.rb new file mode 100644 index 00000000000..ca2b4556b58 --- /dev/null +++ b/app/services/branches/delete_service.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Branches + class DeleteService < BaseService + def execute(branch_name) + repository = project.repository + branch = repository.find_branch(branch_name) + + unless current_user.can?(:push_code, project) + return ServiceResponse.error( + message: 'You dont have push access to repo', + http_status: 405) + end + + unless branch + return ServiceResponse.error( + message: 'No such branch', + http_status: 404) + end + + if repository.rm_branch(current_user, branch_name) + ServiceResponse.success(message: 'Branch was deleted') + else + ServiceResponse.error( + message: 'Failed to remove branch', + http_status: 400) + end + rescue Gitlab::Git::PreReceiveError => ex + ServiceResponse.error(message: ex.message, http_status: 400) + end + end +end diff --git a/app/services/branches/validate_new_service.rb b/app/services/branches/validate_new_service.rb new file mode 100644 index 00000000000..e45183d160f --- /dev/null +++ b/app/services/branches/validate_new_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Branches + class ValidateNewService < BaseService + def initialize(project) + @project = project + end + + def execute(branch_name, force: false) + return error('Branch name is invalid') unless valid_name?(branch_name) + + if branch_exist?(branch_name) && !force + return error('Branch already exists') + end + + success + rescue Gitlab::Git::PreReceiveError => ex + error(ex.message) + end + + private + + def valid_name?(branch_name) + Gitlab::GitRefValidator.validate(branch_name) + end + + def branch_exist?(branch_name) + project.repository.branch_exists?(branch_name) + end + end +end diff --git a/app/services/ci/archive_trace_service.rb b/app/services/ci/archive_trace_service.rb index 8fad9e9c869..f143736ddc1 100644 --- a/app/services/ci/archive_trace_service.rb +++ b/app/services/ci/archive_trace_service.rb @@ -46,10 +46,10 @@ module Ci message: "Failed to archive trace. message: #{error.message}.", job_id: job.id) - Gitlab::Sentry - .track_exception(error, + Gitlab::ErrorTracking + .track_and_raise_for_dev_exception(error, issue_url: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/51502', - extra: { job_id: job.id }) + job_id: job.id ) end end end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 5778a48bce6..ce3a9eb0772 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -16,6 +16,7 @@ module Ci Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules, Gitlab::Ci::Pipeline::Chain::Seed, Gitlab::Ci::Pipeline::Chain::Limit::Size, + Gitlab::Ci::Pipeline::Chain::Validate::External, Gitlab::Ci::Pipeline::Chain::Populate, Gitlab::Ci::Pipeline::Chain::Create, Gitlab::Ci::Pipeline::Chain::Limit::Activity, @@ -57,7 +58,9 @@ module Ci cancel_pending_pipelines if project.auto_cancel_pending_pipelines? pipeline_created_counter.increment(source: source) - pipeline.process! + Ci::ProcessPipelineService + .new(pipeline) + .execute end end diff --git a/app/services/ci/generate_exposed_artifacts_report_service.rb b/app/services/ci/generate_exposed_artifacts_report_service.rb index b9bf580bcbc..1dbcd192279 100644 --- a/app/services/ci/generate_exposed_artifacts_report_service.rb +++ b/app/services/ci/generate_exposed_artifacts_report_service.rb @@ -15,7 +15,7 @@ module Ci data: data } rescue => e - Gitlab::Sentry.track_acceptable_exception(e, extra: { project_id: project.id }) + Gitlab::ErrorTracking.track_exception(e, project_id: project.id) { status: :error, key: key(base_pipeline, head_pipeline), diff --git a/app/services/ci/prepare_build_service.rb b/app/services/ci/prepare_build_service.rb index 3722faeb020..5d024c45e5f 100644 --- a/app/services/ci/prepare_build_service.rb +++ b/app/services/ci/prepare_build_service.rb @@ -13,7 +13,7 @@ module Ci build.enqueue! rescue => e - Gitlab::Sentry.track_acceptable_exception(e, extra: { build_id: build.id }) + Gitlab::ErrorTracking.track_exception(e, build_id: build.id) build.drop(:unmet_prerequisites) end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 039670f58c8..f33cbf7ab29 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -1,14 +1,16 @@ # frozen_string_literal: true module Ci - class ProcessPipelineService < BaseService + class ProcessPipelineService include Gitlab::Utils::StrongMemoize attr_reader :pipeline - def execute(pipeline, trigger_build_ids = nil) + def initialize(pipeline) @pipeline = pipeline + end + def execute(trigger_build_ids = nil) update_retried success = process_stages_without_needs @@ -72,7 +74,7 @@ module Ci def process_build(build, current_status) Gitlab::OptimisticLocking.retry_lock(build) do |subject| - Ci::ProcessBuildService.new(project, @user) + Ci::ProcessBuildService.new(project, build.user) .execute(subject, current_status) end end @@ -129,5 +131,9 @@ module Ci .update_all(retried: true) if latest_statuses.any? end # rubocop: enable CodeReuse/ActiveRecord + + def project + pipeline.project + end end end diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index 30e2a66e04a..57c0cdd0602 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -128,13 +128,13 @@ module Ci end def track_exception_for_build(ex, build) - Gitlab::Sentry.track_acceptable_exception(ex, extra: { + Gitlab::ErrorTracking.track_exception(ex, build_id: build.id, build_name: build.name, build_stage: build.stage, pipeline_id: build.pipeline_id, project_id: build.project_id - }) + ) end # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb index 42a13367a99..7d01de9ee68 100644 --- a/app/services/ci/retry_pipeline_service.rb +++ b/app/services/ci/retry_pipeline_service.rb @@ -9,13 +9,23 @@ module Ci raise Gitlab::Access::AccessDeniedError end - pipeline.retryable_builds.find_each do |build| + needs = Set.new + + pipeline.retryable_builds.preload_needs.find_each do |build| next unless can?(current_user, :update_build, build) Ci::RetryBuildService.new(project, current_user) .reprocess!(build) + + needs += build.needs.map(&:name) end + # In a DAG, the dependencies may have already completed. Figure out + # which builds have succeeded and use them to update the pipeline. If we don't + # do this, then builds will be stuck in the created state since their dependencies + # will never run. + completed_build_ids = pipeline.find_successful_build_ids_by_names(needs) if needs.any? + pipeline.builds.latest.skipped.find_each do |skipped| retry_optimistic_lock(skipped) { |build| build.process } end @@ -24,7 +34,9 @@ module Ci .new(project, current_user) .close_all(pipeline) - pipeline.process! + Ci::ProcessPipelineService + .new(pipeline) + .execute(completed_build_ids) end end end diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb index 3e7f55f0c63..57bc8bc0d9b 100644 --- a/app/services/clusters/applications/base_helm_service.rb +++ b/app/services/clusters/applications/base_helm_service.rb @@ -21,14 +21,7 @@ module Clusters group_ids: app.cluster.group_ids } - logger_meta = meta.merge( - exception: error.class.name, - message: error.message, - backtrace: Gitlab::Profiler.clean_backtrace(error.backtrace) - ) - - logger.error(logger_meta) - Gitlab::Sentry.track_acceptable_exception(error, extra: meta) + Gitlab::ErrorTracking.track_exception(error, meta) end def log_event(event) @@ -68,8 +61,8 @@ module Clusters @update_command ||= app.update_command end - def upgrade_command(new_values = "") - app.upgrade_command(new_values) + def patch_command(new_values = "") + app.patch_command(new_values) end end end diff --git a/app/services/clusters/applications/ingress_modsecurity_usage_service.rb b/app/services/clusters/applications/ingress_modsecurity_usage_service.rb new file mode 100644 index 00000000000..4aac8bb3cbd --- /dev/null +++ b/app/services/clusters/applications/ingress_modsecurity_usage_service.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# rubocop: disable CodeReuse/ActiveRecord +module Clusters + module Applications + ## + # This service measures usage of the Modsecurity Web Application Firewall across the entire + # instance's deployed environments. + # + # The default configuration is`AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE=DetectionOnly` so we + # measure non-default values via definition of either ci_variables or ci_pipeline_variables. + # Since both these values are encrypted, we must decrypt and count them in memory. + # + # NOTE: this service is an approximation as it does not yet take into account `environment_scope` or `ci_group_variables`. + ## + class IngressModsecurityUsageService + ADO_MODSEC_KEY = "AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE" + + def initialize(blocking_count: 0, disabled_count: 0) + @blocking_count = blocking_count + @disabled_count = disabled_count + end + + def execute + conditions = -> { merge(::Environment.available).merge(::Deployment.success).where(key: ADO_MODSEC_KEY) } + + ci_pipeline_var_enabled = + ::Ci::PipelineVariable + .joins(pipeline: { environments: :last_visible_deployment }) + .merge(conditions) + .order('deployments.environment_id, deployments.id DESC') + + ci_var_enabled = + ::Ci::Variable + .joins(project: { environments: :last_visible_deployment }) + .merge(conditions) + .merge( + # Give priority to pipeline variables by excluding from dataset + ::Ci::Variable.joins(project: :environments).where.not( + environments: { id: ci_pipeline_var_enabled.select('DISTINCT ON (deployments.environment_id) deployments.environment_id') } + ) + ).select('DISTINCT ON (deployments.environment_id) ci_variables.*') + + sum_modsec_config_counts( + ci_pipeline_var_enabled.select('DISTINCT ON (deployments.environment_id) ci_pipeline_variables.*') + ) + sum_modsec_config_counts(ci_var_enabled) + + { + ingress_modsecurity_blocking: @blocking_count, + ingress_modsecurity_disabled: @disabled_count + } + end + + private + + # These are encrypted so we must decrypt and count in memory + def sum_modsec_config_counts(dataset) + dataset.each do |var| + case var.value + when "On" then @blocking_count += 1 + when "Off" then @disabled_count += 1 + # `else` could be default or any unsupported user input + end + end + end + end + end +end diff --git a/app/services/clusters/aws/authorize_role_service.rb b/app/services/clusters/aws/authorize_role_service.rb new file mode 100644 index 00000000000..6eafce0597e --- /dev/null +++ b/app/services/clusters/aws/authorize_role_service.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Clusters + module Aws + class AuthorizeRoleService + attr_reader :user + + Response = Struct.new(:status, :body) + + ERRORS = [ + ActiveRecord::RecordInvalid, + Clusters::Aws::FetchCredentialsService::MissingRoleError, + ::Aws::Errors::MissingCredentialsError, + ::Aws::STS::Errors::ServiceError + ].freeze + + def initialize(user, params:) + @user = user + @params = params + end + + def execute + @role = create_or_update_role! + + Response.new(:ok, credentials) + rescue *ERRORS + Response.new(:unprocessable_entity, {}) + end + + private + + attr_reader :role, :params + + def create_or_update_role! + if role = user.aws_role + role.update!(params) + + role + else + user.create_aws_role!(params) + end + end + + def credentials + Clusters::Aws::FetchCredentialsService.new(role).execute + end + end + end +end diff --git a/app/services/clusters/aws/fetch_credentials_service.rb b/app/services/clusters/aws/fetch_credentials_service.rb index 2724d4b657b..33efc4cc120 100644 --- a/app/services/clusters/aws/fetch_credentials_service.rb +++ b/app/services/clusters/aws/fetch_credentials_service.rb @@ -7,9 +7,8 @@ module Clusters MissingRoleError = Class.new(StandardError) - def initialize(provision_role, region:, provider: nil) + def initialize(provision_role, provider: nil) @provision_role = provision_role - @region = region @provider = provider end @@ -20,13 +19,14 @@ module Clusters client: client, role_arn: provision_role.role_arn, role_session_name: session_name, - external_id: provision_role.role_external_id + external_id: provision_role.role_external_id, + policy: session_policy ).credentials end private - attr_reader :provider, :region + attr_reader :provider def client ::Aws::STS::Client.new(credentials: gitlab_credentials, region: region) @@ -44,6 +44,26 @@ module Clusters Gitlab::CurrentSettings.eks_secret_access_key end + def region + provider&.region || Clusters::Providers::Aws::DEFAULT_REGION + end + + ## + # If we haven't created a provider record yet, + # we restrict ourselves to read only access so + # that we can safely expose credentials to the + # frontend (to be used when populating the + # creation form). + def session_policy + if provider.nil? + File.read(read_only_policy) + end + end + + def read_only_policy + Rails.root.join('vendor', 'aws', 'iam', "eks_cluster_read_only_policy.json") + end + def session_name if provider.present? "gitlab-eks-cluster-#{provider.cluster_id}-user-#{provision_role.user_id}" diff --git a/app/services/clusters/aws/proxy_service.rb b/app/services/clusters/aws/proxy_service.rb deleted file mode 100644 index df8fc480005..00000000000 --- a/app/services/clusters/aws/proxy_service.rb +++ /dev/null @@ -1,134 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Aws - class ProxyService - DEFAULT_REGION = 'us-east-1' - - BadRequest = Class.new(StandardError) - Response = Struct.new(:status, :body) - - def initialize(role, params:) - @role = role - @params = params - end - - def execute - api_response = request_from_api! - - Response.new(:ok, api_response.to_hash) - rescue *service_errors - Response.new(:bad_request, {}) - end - - private - - attr_reader :role, :params - - def request_from_api! - case requested_resource - when 'key_pairs' - ec2_client.describe_key_pairs - - when 'instance_types' - instance_types - - when 'roles' - iam_client.list_roles - - when 'regions' - ec2_client.describe_regions - - when 'security_groups' - raise BadRequest unless vpc_id.present? - - ec2_client.describe_security_groups(vpc_filter) - - when 'subnets' - raise BadRequest unless vpc_id.present? - - ec2_client.describe_subnets(vpc_filter) - - when 'vpcs' - ec2_client.describe_vpcs - - else - raise BadRequest - end - end - - def requested_resource - params[:resource] - end - - def vpc_id - params[:vpc_id] - end - - def region - params[:region] || DEFAULT_REGION - end - - def vpc_filter - { - filters: [{ - name: "vpc-id", - values: [vpc_id] - }] - } - end - - ## - # Unfortunately the EC2 API doesn't provide a list of - # possible instance types. There is a workaround, using - # the Pricing API, but instead of requiring the - # user to grant extra permissions for this we use the - # values that validate the CloudFormation template. - def instance_types - { - instance_types: cluster_stack_instance_types.map { |type| Hash(instance_type_name: type) } - } - end - - def cluster_stack_instance_types - YAML.safe_load(stack_template).dig('Parameters', 'NodeInstanceType', 'AllowedValues') - end - - def stack_template - File.read(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml')) - end - - def ec2_client - ::Aws::EC2::Client.new(client_options) - end - - def iam_client - ::Aws::IAM::Client.new(client_options) - end - - def credentials - Clusters::Aws::FetchCredentialsService.new(role, region: region).execute - end - - def client_options - { - credentials: credentials, - region: region, - http_open_timeout: 5, - http_read_timeout: 10 - } - end - - def service_errors - [ - BadRequest, - Clusters::Aws::FetchCredentialsService::MissingRoleError, - ::Aws::Errors::MissingCredentialsError, - ::Aws::EC2::Errors::ServiceError, - ::Aws::IAM::Errors::ServiceError, - ::Aws::STS::Errors::ServiceError - ] - end - end - end -end diff --git a/app/services/clusters/cleanup/app_service.rb b/app/services/clusters/cleanup/app_service.rb new file mode 100644 index 00000000000..a7e29c78ea0 --- /dev/null +++ b/app/services/clusters/cleanup/app_service.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Clusters + module Cleanup + class AppService < Clusters::Cleanup::BaseService + def execute + persisted_applications = @cluster.persisted_applications + + persisted_applications.each do |app| + next unless app.available? + next unless app.can_uninstall? + + log_event(:uninstalling_app, application: app.class.application_name) + uninstall_app_async(app) + end + + # Keep calling the worker untill all dependencies are uninstalled + return schedule_next_execution(Clusters::Cleanup::AppWorker) if persisted_applications.any? + + log_event(:schedule_remove_project_namespaces) + cluster.continue_cleanup! + end + + private + + def uninstall_app_async(application) + application.make_scheduled! + + Clusters::Applications::UninstallWorker.perform_async(application.name, application.id) + end + end + end +end diff --git a/app/services/clusters/cleanup/base_service.rb b/app/services/clusters/cleanup/base_service.rb new file mode 100644 index 00000000000..f99e54cfc40 --- /dev/null +++ b/app/services/clusters/cleanup/base_service.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Clusters + module Cleanup + class BaseService + DEFAULT_EXECUTION_INTERVAL = 1.minute + + def initialize(cluster, execution_count = 0) + @cluster = cluster + @execution_count = execution_count + end + + private + + attr_reader :cluster + + def logger + @logger ||= Gitlab::Kubernetes::Logger.build + end + + def log_event(event, extra_data = {}) + meta = { + service: self.class.name, + cluster_id: cluster.id, + execution_count: @execution_count, + event: event + } + + logger.info(meta.merge(extra_data)) + end + + def schedule_next_execution(worker_class) + log_event(:scheduling_execution, next_execution: @execution_count + 1) + worker_class.perform_in(execution_interval, cluster.id, @execution_count + 1) + end + + # Override this method to customize the execution interval + def execution_interval + DEFAULT_EXECUTION_INTERVAL + end + end + end +end diff --git a/app/services/clusters/cleanup/project_namespace_service.rb b/app/services/clusters/cleanup/project_namespace_service.rb new file mode 100644 index 00000000000..7621be565ff --- /dev/null +++ b/app/services/clusters/cleanup/project_namespace_service.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Clusters + module Cleanup + class ProjectNamespaceService < BaseService + KUBERNETES_NAMESPACE_BATCH_SIZE = 100 + + def execute + delete_project_namespaces_in_batches + + # Keep calling the worker untill all namespaces are deleted + if cluster.kubernetes_namespaces.exists? + return schedule_next_execution(Clusters::Cleanup::ProjectNamespaceWorker) + end + + cluster.continue_cleanup! + end + + private + + def delete_project_namespaces_in_batches + kubernetes_namespaces_batch = cluster.kubernetes_namespaces.first(KUBERNETES_NAMESPACE_BATCH_SIZE) + + kubernetes_namespaces_batch.each do |kubernetes_namespace| + log_event(:deleting_project_namespace, namespace: kubernetes_namespace.namespace) + + begin + kubeclient_delete_namespace(kubernetes_namespace) + rescue Kubeclient::HttpError + next + end + + kubernetes_namespace.destroy! + end + end + + def kubeclient_delete_namespace(kubernetes_namespace) + cluster.kubeclient.delete_namespace(kubernetes_namespace.namespace) + rescue Kubeclient::ResourceNotFoundError + # no-op: nothing to delete + end + end + end +end diff --git a/app/services/clusters/cleanup/service_account_service.rb b/app/services/clusters/cleanup/service_account_service.rb new file mode 100644 index 00000000000..d60bd76d388 --- /dev/null +++ b/app/services/clusters/cleanup/service_account_service.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Clusters + module Cleanup + class ServiceAccountService < BaseService + def execute + delete_gitlab_service_account + + log_event(:destroying_cluster) + + cluster.destroy! + end + + private + + def delete_gitlab_service_account + log_event(:deleting_gitlab_service_account) + + cluster.kubeclient.delete_service_account( + ::Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAME, + ::Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE + ) + rescue Kubeclient::ResourceNotFoundError + end + end + end +end diff --git a/app/services/clusters/kubernetes/kubernetes.rb b/app/services/clusters/kubernetes.rb index d29519999b2..59cb1c4b3a9 100644 --- a/app/services/clusters/kubernetes/kubernetes.rb +++ b/app/services/clusters/kubernetes.rb @@ -12,5 +12,8 @@ module Clusters GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding' GITLAB_CROSSPLANE_DATABASE_ROLE_NAME = 'gitlab-crossplane-database-role' GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME = 'gitlab-crossplane-database-rolebinding' + GITLAB_KNATIVE_VERSION_ROLE_NAME = 'gitlab-knative-version-role' + GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME = 'gitlab-knative-version-rolebinding' + KNATIVE_SERVING_NAMESPACE = 'knative-serving' end end diff --git a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb index d798dcdcfd3..046046bf5a3 100644 --- a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb +++ b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb @@ -49,8 +49,14 @@ module Clusters create_or_update_knative_serving_role create_or_update_knative_serving_role_binding + create_or_update_crossplane_database_role create_or_update_crossplane_database_role_binding + + return unless knative_serving_namespace + + create_or_update_knative_version_role + create_or_update_knative_version_role_binding end private @@ -64,6 +70,12 @@ module Clusters ).ensure_exists! end + def knative_serving_namespace + kubeclient.get_namespace(Clusters::Kubernetes::KNATIVE_SERVING_NAMESPACE) + rescue Kubeclient::ResourceNotFoundError + nil + end + def create_role_or_cluster_role_binding if namespace_creator kubeclient.create_or_update_role_binding(role_binding_resource) @@ -88,6 +100,14 @@ module Clusters kubeclient.update_role_binding(crossplane_database_role_binding_resource) end + def create_or_update_knative_version_role + kubeclient.update_cluster_role(knative_version_role_resource) + end + + def create_or_update_knative_version_role_binding + kubeclient.update_cluster_role_binding(knative_version_role_binding_resource) + end + def service_account_resource Gitlab::Kubernetes::ServiceAccount.new( service_account_name, @@ -166,6 +186,27 @@ module Clusters service_account_name: service_account_name ).generate end + + def knative_version_role_resource + Gitlab::Kubernetes::ClusterRole.new( + name: Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME, + rules: [{ + apiGroups: %w(apps), + resources: %w(deployments), + verbs: %w(list get) + }] + ).generate + end + + def knative_version_role_binding_resource + subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }] + + Gitlab::Kubernetes::ClusterRoleBinding.new( + Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_BINDING_NAME, + Clusters::Kubernetes::GITLAB_KNATIVE_VERSION_ROLE_NAME, + subjects + ).generate + end end end end diff --git a/app/services/cohorts_service.rb b/app/services/cohorts_service.rb index dbbe89ef260..03be87f4cc1 100644 --- a/app/services/cohorts_service.rb +++ b/app/services/cohorts_service.rb @@ -38,7 +38,7 @@ class CohortsService { registration_month: registration_month, - activity_months: activity_months, + activity_months: activity_months[1..-1], total: activity_months.first[:total], inactive: inactive } diff --git a/app/services/commits/commit_patch_service.rb b/app/services/commits/commit_patch_service.rb index 49113c3c691..4fa6c30e901 100644 --- a/app/services/commits/commit_patch_service.rb +++ b/app/services/commits/commit_patch_service.rb @@ -32,7 +32,7 @@ module Commits end def prepare_branch! - branch_result = CreateBranchService.new(project, current_user) + branch_result = ::Branches::CreateService.new(project, current_user) .execute(@branch_name, @start_branch) if branch_result[:status] != :success diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index b42494563b2..bd238605ac1 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -101,7 +101,7 @@ module Commits end def validate_new_branch_name! - result = ValidateNewBranchService.new(project, current_user).execute(@branch_name, force: force?) + result = ::Branches::ValidateNewService.new(project).execute(@branch_name, force: force?) if result[:status] == :error raise_error("Something went wrong when we tried to create '#{@branch_name}' for you: #{result[:message]}") diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb index 1c828234f1b..6fde9abfdb0 100644 --- a/app/services/concerns/users/participable_service.rb +++ b/app/services/concerns/users/participable_service.rb @@ -55,7 +55,8 @@ module Users username: group.full_path, name: group.full_name, avatar_url: group.avatar_url, - count: group_counts.fetch(group.id, 0) + count: group_counts.fetch(group.id, 0), + mentionsDisabled: group.mentions_disabled } end end diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb deleted file mode 100644 index d58cb0f9e2b..00000000000 --- a/app/services/create_branch_service.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -class CreateBranchService < BaseService - def execute(branch_name, ref, create_master_if_empty: true) - create_master_branch if create_master_if_empty && project.empty_repo? - - result = ValidateNewBranchService.new(project, current_user) - .execute(branch_name) - - return result if result[:status] == :error - - new_branch = repository.add_branch(current_user, branch_name, ref) - - if new_branch - success(new_branch) - else - error("Invalid reference name: #{branch_name}") - end - rescue Gitlab::Git::PreReceiveError => ex - error(ex.message) - end - - def success(branch) - super().merge(branch: branch) - end - - private - - def create_master_branch - project.repository.create_file( - current_user, - '/README.md', - '', - message: 'Add README.md', - branch_name: 'master' - ) - end -end diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb index 0aa76df35ba..eacea7d94c7 100644 --- a/app/services/create_snippet_service.rb +++ b/app/services/create_snippet_service.rb @@ -21,7 +21,11 @@ class CreateSnippetService < BaseService spam_check(snippet, current_user) - if snippet.save + snippet_saved = snippet.with_transaction_returning_status do + snippet.save && snippet.store_mentions! + end + + if snippet_saved UserAgentDetailService.new(snippet, @request).create Gitlab::UsageDataCounters::SnippetCounter.count(:create) end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb deleted file mode 100644 index fd41ce54486..00000000000 --- a/app/services/delete_branch_service.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -class DeleteBranchService < BaseService - def execute(branch_name) - repository = project.repository - branch = repository.find_branch(branch_name) - - unless current_user.can?(:push_code, project) - return ServiceResponse.error( - message: 'You dont have push access to repo', - http_status: 405) - end - - unless branch - return ServiceResponse.error( - message: 'No such branch', - http_status: 404) - end - - if repository.rm_branch(current_user, branch_name) - ServiceResponse.success(message: 'Branch was deleted') - else - ServiceResponse.error( - message: 'Failed to remove branch', - http_status: 400) - end - rescue Gitlab::Git::PreReceiveError => ex - ServiceResponse.error(message: ex.message, http_status: 400) - end -end diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb deleted file mode 100644 index 80de897e94b..00000000000 --- a/app/services/delete_merged_branches_service.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -class DeleteMergedBranchesService < BaseService - def async_execute - DeleteMergedBranchesWorker.perform_async(project.id, current_user.id) - end - - def execute - raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project) - - branches = project.repository.merged_branch_names - # Prevent deletion of branches relevant to open merge requests - branches -= merge_request_branch_names - # Prevent deletion of protected branches - branches = branches.reject { |branch| ProtectedBranch.protected?(project, branch) } - - branches.each do |branch| - DeleteBranchService.new(project, current_user).execute(branch) - end - end - - private - - # rubocop: disable CodeReuse/ActiveRecord - def merge_request_branch_names - # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY - source_names = project.origin_merge_requests.opened.reorder(nil).distinct.pluck(:source_branch) - target_names = project.merge_requests.opened.reorder(nil).distinct.pluck(:target_branch) - (source_names + target_names).uniq - end - # rubocop: enable CodeReuse/ActiveRecord -end diff --git a/app/services/deployments/after_create_service.rb b/app/services/deployments/after_create_service.rb index e0a4e5419cc..1d9cb666cff 100644 --- a/app/services/deployments/after_create_service.rb +++ b/app/services/deployments/after_create_service.rb @@ -29,6 +29,7 @@ module Deployments environment.external_url = url end + renew_auto_stop_in environment.fire_state_event(action) if environment.save && !environment.stopped? @@ -63,6 +64,12 @@ module Deployments def action environment_options[:action] || 'start' end + + def renew_auto_stop_in + return unless deployable + + environment.auto_stop_in = deployable.environment_auto_stop_in + end end end diff --git a/app/services/deployments/create_service.rb b/app/services/deployments/create_service.rb index 89e3f7c8b83..7355747d778 100644 --- a/app/services/deployments/create_service.rb +++ b/app/services/deployments/create_service.rb @@ -11,15 +11,17 @@ module Deployments end def execute - create_deployment.tap do |deployment| - AfterCreateService.new(deployment).execute if deployment.persisted? + environment.deployments.build(deployment_attributes).tap do |deployment| + # Deployment#change_status already saves the model, so we only need to + # call #save ourselves if no status is provided. + if (status = params[:status]) + deployment.update_status(status) + else + deployment.save + end end end - def create_deployment - environment.deployments.create(deployment_attributes) - end - def deployment_attributes # We use explicit parameters here so we never by accident allow parameters # to be set that one should not be able to set (e.g. the row ID). @@ -31,8 +33,7 @@ module Deployments tag: params[:tag], sha: params[:sha], user: current_user, - on_stop: params[:on_stop], - status: params[:status] + on_stop: params[:on_stop] } end end diff --git a/app/services/deployments/update_service.rb b/app/services/deployments/update_service.rb index 97b233f16a7..b8f8740c9b9 100644 --- a/app/services/deployments/update_service.rb +++ b/app/services/deployments/update_service.rb @@ -10,22 +10,7 @@ module Deployments end def execute - # A regular update() does not trigger the state machine transitions, which - # we need to ensure merge requests are linked when changing the status to - # success. To work around this we use this case statment, using the right - # event methods to trigger the transition hooks. - case params[:status] - when 'running' - deployment.run - when 'success' - deployment.succeed - when 'failed' - deployment.drop - when 'canceled' - deployment.cancel - else - false - end + deployment.update_status(params[:status]) end end end diff --git a/app/services/environments/reset_auto_stop_service.rb b/app/services/environments/reset_auto_stop_service.rb new file mode 100644 index 00000000000..237629fda79 --- /dev/null +++ b/app/services/environments/reset_auto_stop_service.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Environments + class ResetAutoStopService < ::BaseService + def execute(environment) + return error(_('Failed to cancel auto stop because you do not have permission to update the environment.')) unless can_update_environment?(environment) + return error(_('Failed to cancel auto stop because the environment is not set as auto stop.')) unless environment.auto_stop_at? + + if environment.reset_auto_stop + success + else + error(_('Failed to cancel auto stop because failed to update the environment.')) + end + end + + private + + def can_update_environment?(environment) + can?(current_user, :update_environment, environment) + end + end +end diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb index 2e8c401b8ef..132e9dfa7bd 100644 --- a/app/services/error_tracking/list_issues_service.rb +++ b/app/services/error_tracking/list_issues_service.rb @@ -4,6 +4,7 @@ module ErrorTracking class ListIssuesService < ErrorTracking::BaseService DEFAULT_ISSUE_STATUS = 'unresolved' DEFAULT_LIMIT = 20 + DEFAULT_SORT = 'last_seen' def external_url project_error_tracking_setting&.sentry_external_url @@ -12,11 +13,17 @@ module ErrorTracking private def fetch - project_error_tracking_setting.list_sentry_issues(issue_status: issue_status, limit: limit) + project_error_tracking_setting.list_sentry_issues( + issue_status: issue_status, + limit: limit, + search_term: params[:search_term].presence, + sort: sort, + cursor: params[:cursor].presence + ) end def parse_response(response) - { issues: response[:issues] } + response.slice(:issues, :pagination) end def issue_status @@ -26,5 +33,9 @@ module ErrorTracking def limit params[:limit] || DEFAULT_LIMIT end + + def sort + params[:sort] || DEFAULT_SORT + end end end diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index 0801fd4d03f..d935d9e8cdc 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -85,12 +85,36 @@ module Git before: oldrev, after: newrev, ref: ref, + variables_attributes: generate_vars_from_push_options || [], push_options: params[:push_options] || {}, checkout_sha: Gitlab::DataBuilder::Push.checkout_sha( project.repository, newrev, ref) } end + def ci_variables_from_push_options + strong_memoize(:ci_variables_from_push_options) do + params[:push_options]&.deep_symbolize_keys&.dig(:ci, :variable) + end + end + + def generate_vars_from_push_options + return [] unless ci_variables_from_push_options + + ci_variables_from_push_options.map do |var_definition, _count| + key, value = var_definition.to_s.split("=", 2) + + # Accept only valid format. We ignore the following formats + # 1. "=123". In this case, `key` will be an empty string + # 2. "FOO". In this case, `value` will be nil. + # However, the format "FOO=" will result in key beign `FOO` and value + # being an empty string. This is acceptable. + next if key.blank? || value.nil? + + { "key" => key, "variable_type" => "env_var", "secret_value" => value } + end.compact + end + def push_data_params(commits:, with_changed_files: true) { oldrev: oldrev, diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index 273a12f386a..bbb3c2ad050 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -4,19 +4,18 @@ module Issuable class BulkUpdateService include Gitlab::Allowable - attr_accessor :current_user, :params + attr_accessor :parent, :current_user, :params - def initialize(user = nil, params = {}) - @current_user, @params = user, params.dup + def initialize(parent, user = nil, params = {}) + @parent, @current_user, @params = parent, user, params.dup end - # rubocop: disable CodeReuse/ActiveRecord def execute(type) model_class = type.classify.constantize update_class = type.classify.pluralize.constantize::UpdateService ids = params.delete(:issuable_ids).split(",") - items = model_class.where(id: ids) + items = find_issuables(parent, model_class, ids) permitted_attrs(type).each do |key| params.delete(key) unless params[key].present? @@ -37,7 +36,6 @@ module Issuable success: !items.count.zero? } end - # rubocop: enable CodeReuse/ActiveRecord private @@ -50,5 +48,15 @@ module Issuable attrs.push(:assignee_id) end end + + def find_issuables(parent, model_class, ids) + if parent.is_a?(Project) + model_class.id_in(ids).of_projects(parent) + elsif parent.is_a?(Group) + model_class.id_in(ids).of_projects(parent.all_projects) + end + end end end + +Issuable::BulkUpdateService.prepend_if_ee('EE::Issuable::BulkUpdateService') diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb index 10c89c62bf1..1f5d83917cc 100644 --- a/app/services/issuable/clone/attributes_rewriter.rb +++ b/app/services/issuable/clone/attributes_rewriter.rb @@ -10,7 +10,13 @@ module Issuable end def execute - new_entity.update(milestone: cloneable_milestone, labels: cloneable_labels) + update_attributes = { labels: cloneable_labels } + + milestone = cloneable_milestone + update_attributes[:milestone] = milestone if milestone.present? + + new_entity.update(update_attributes) + copy_resource_label_events end diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb index a170a4dcae2..846b881e819 100644 --- a/app/services/issuable/common_system_notes_service.rb +++ b/app/services/issuable/common_system_notes_service.rb @@ -7,20 +7,24 @@ module Issuable def execute(issuable, old_labels: [], is_update: true) @issuable = issuable - if is_update - if issuable.previous_changes.include?('title') - create_title_change_note(issuable.previous_changes['title'].first) + # We disable touch so that created system notes do not update + # the noteable's updated_at field + ActiveRecord::Base.no_touching do + if is_update + if issuable.previous_changes.include?('title') + create_title_change_note(issuable.previous_changes['title'].first) + end + + handle_description_change_note + + handle_time_tracking_note if issuable.is_a?(TimeTrackable) + create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked') end - handle_description_change_note - - handle_time_tracking_note if issuable.is_a?(TimeTrackable) - create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked') + create_due_date_note if issuable.previous_changes.include?('due_date') + create_milestone_note if issuable.previous_changes.include?('milestone_id') + create_labels_note(old_labels) if old_labels && issuable.labels != old_labels end - - create_due_date_note if issuable.previous_changes.include?('due_date') - create_milestone_note if issuable.previous_changes.include?('milestone_id') - create_labels_note(old_labels) if old_labels && issuable.labels != old_labels end private diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 8a79c5f889d..6cb84458d9b 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -163,10 +163,12 @@ class IssuableBaseService < BaseService before_create(issuable) - if issuable.save - ActiveRecord::Base.no_touching do - Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, is_update: false) - end + issuable_saved = issuable.with_transaction_returning_status do + issuable.save && issuable.store_mentions! + end + + if issuable_saved + Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, is_update: false) after_create(issuable) execute_hooks(issuable) @@ -226,11 +228,12 @@ class IssuableBaseService < BaseService update_project_counters = issuable.project && update_project_counter_caches?(issuable) ensure_milestone_available(issuable) - if issuable.with_transaction_returning_status { issuable.save(touch: should_touch) } - # We do not touch as it will affect a update on updated_at field - ActiveRecord::Base.no_touching do - Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: old_associations[:labels]) - end + issuable_saved = issuable.with_transaction_returning_status do + issuable.save(touch: should_touch) && issuable.store_mentions! + end + + if issuable_saved + Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: old_associations[:labels]) handle_changes(issuable, old_associations: old_associations) @@ -264,10 +267,7 @@ class IssuableBaseService < BaseService before_update(issuable, skip_spam_check: true) if issuable.with_transaction_returning_status { issuable.save } - # We do not touch as it will affect a update on updated_at field - ActiveRecord::Base.no_touching do - Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: nil) - end + Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: nil) handle_task_changes(issuable) invalidate_cache_counts(issuable, users: issuable.assignees.to_a) @@ -397,7 +397,7 @@ class IssuableBaseService < BaseService end def update_project_counter_caches?(issuable) - issuable.state_changed? + issuable.state_id_changed? end def parent diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 48ed5afbc2a..974f7e598ca 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -36,3 +36,5 @@ module Issues end end end + +Issues::BaseService.prepend_if_ee('EE::Issues::BaseService') diff --git a/app/services/issues/duplicate_service.rb b/app/services/issues/duplicate_service.rb index 82c226f601e..c936d75e277 100644 --- a/app/services/issues/duplicate_service.rb +++ b/app/services/issues/duplicate_service.rb @@ -25,3 +25,5 @@ module Issues end end end + +Issues::DuplicateService.prepend_if_ee('EE::Issues::DuplicateService') diff --git a/app/services/issues/zoom_link_service.rb b/app/services/issues/zoom_link_service.rb index 023d7080e88..9572cf50564 100644 --- a/app/services/issues/zoom_link_service.rb +++ b/app/services/issues/zoom_link_service.rb @@ -13,30 +13,29 @@ module Issues if can_add_link? && (link = parse_link(link)) begin add_zoom_meeting(link) - success(_('Zoom meeting added')) rescue ActiveRecord::RecordNotUnique - error(_('Failed to add a Zoom meeting')) + error(message: _('Failed to add a Zoom meeting')) end else - error(_('Failed to add a Zoom meeting')) + error(message: _('Failed to add a Zoom meeting')) end end def remove_link if can_remove_link? remove_zoom_meeting - success(_('Zoom meeting removed')) + success(message: _('Zoom meeting removed')) else - error(_('Failed to remove a Zoom meeting')) + error(message: _('Failed to remove a Zoom meeting')) end end def can_add_link? - can_update_issue? && !@added_meeting + can_change_link? && !@added_meeting end def can_remove_link? - can_update_issue? && !!@added_meeting + can_change_link? && @issue.persisted? && !!@added_meeting end def parse_link(link) @@ -56,14 +55,29 @@ module Issues end def add_zoom_meeting(link) - ZoomMeeting.create( + zoom_meeting = new_zoom_meeting(link) + response = + if @issue.persisted? + # Save the meeting directly since we only want to update one meeting, not all + zoom_meeting.save + success(message: _('Zoom meeting added')) + else + success(message: _('Zoom meeting added'), payload: { zoom_meetings: [zoom_meeting] }) + end + + track_meeting_added_event + SystemNoteService.zoom_link_added(@issue, @project, current_user) + + response + end + + def new_zoom_meeting(link) + ZoomMeeting.new( issue: @issue, - project: @issue.project, + project: @project, issue_status: :added, url: link ) - track_meeting_added_event - SystemNoteService.zoom_link_added(@issue, @project, current_user) end def remove_zoom_meeting @@ -72,16 +86,20 @@ module Issues SystemNoteService.zoom_link_removed(@issue, @project, current_user) end - def success(message) - ServiceResponse.success(message: message) + def success(message:, payload: nil) + ServiceResponse.success(message: message, payload: payload) end - def error(message) + def error(message:) ServiceResponse.error(message: message) end - def can_update_issue? - can?(current_user, :update_issue, project) + def can_change_link? + if @issue.persisted? + can?(current_user, :update_issue, @project) + else + can?(current_user, :create_issue, @project) + end end end end diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb index 200a34cae04..95fb99d3e7a 100644 --- a/app/services/merge_requests/create_from_issue_service.rb +++ b/app/services/merge_requests/create_from_issue_service.rb @@ -19,7 +19,7 @@ module MergeRequests return error('Not allowed to create merge request') unless can_create_merge_request? return error('Invalid issue iid') unless @issue_iid.present? && issue.present? - result = CreateBranchService.new(target_project, current_user).execute(branch_name, ref) + result = ::Branches::CreateService.new(target_project, current_user).execute(branch_name, ref) return result if result[:status] == :error new_merge_request = create(merge_request) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index a45b4f1142e..4a109fe4e16 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -62,8 +62,6 @@ module MergeRequests end def updated_check! - return unless Feature.enabled?(:validate_merge_sha, merge_request.target_project, default_enabled: false) - unless source_matches? raise_error('Branch has been updated since the merge was requested. '\ 'Please review the changes.') @@ -101,7 +99,7 @@ module MergeRequests log_info("Post merge finished on JID #{merge_jid} with state #{state}") if delete_source_branch? - DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) + ::Branches::DeleteService.new(@merge_request.source_project, branch_deletion_user) .execute(merge_request.source_branch) end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index bd3fcf85a62..396ddec6383 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -106,7 +106,7 @@ module MergeRequests filter_merge_requests(merge_requests).each do |merge_request| if branch_and_project_match?(merge_request) || @push.force_push? merge_request.reload_diff(current_user) - elsif merge_request.includes_any_commits?(push_commit_ids) + elsif merge_request.merge_request_diff.includes_any_commits?(push_commit_ids) merge_request.reload_diff(current_user) end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 8a6a7119508..1dc5503d368 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -87,7 +87,7 @@ module MergeRequests merge_request.update(merge_error: nil) - if merge_request.head_pipeline && merge_request.head_pipeline.active? + if merge_request.head_pipeline_active? AutoMergeService.new(project, current_user, { sha: last_diff_sha }).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS) else merge_request.merge_async(current_user.id, { sha: last_diff_sha }) diff --git a/app/services/metrics/dashboard/base_embed_service.rb b/app/services/metrics/dashboard/base_embed_service.rb index 8bb5f4892cb..8aef9873ac1 100644 --- a/app/services/metrics/dashboard/base_embed_service.rb +++ b/app/services/metrics/dashboard/base_embed_service.rb @@ -13,7 +13,7 @@ module Metrics def dashboard_path params[:dashboard_path].presence || - ::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH + ::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH end def group diff --git a/app/services/metrics/dashboard/custom_metric_embed_service.rb b/app/services/metrics/dashboard/custom_metric_embed_service.rb index 79a556b1695..9e616f4e379 100644 --- a/app/services/metrics/dashboard/custom_metric_embed_service.rb +++ b/app/services/metrics/dashboard/custom_metric_embed_service.rb @@ -40,7 +40,7 @@ module Metrics # All custom metrics are displayed on the system dashboard. # Nil is acceptable as we'll default to the system dashboard. def valid_dashboard?(dashboard) - dashboard.nil? || ::Metrics::Dashboard::SystemDashboardService.system_dashboard?(dashboard) + dashboard.nil? || ::Metrics::Dashboard::SystemDashboardService.matching_dashboard?(dashboard) end end diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb index 60591e9a6f3..44b58ad9729 100644 --- a/app/services/metrics/dashboard/grafana_metric_embed_service.rb +++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb @@ -133,7 +133,7 @@ module Metrics def uid_regex base_url = @project.grafana_integration.grafana_url.chomp('/') - %r{(#{Regexp.escape(base_url)}\/d\/(?<uid>\w+)\/)}x + %r{^(#{Regexp.escape(base_url)}\/d\/(?<uid>.+)\/)}x end end diff --git a/app/services/metrics/dashboard/pod_dashboard_service.rb b/app/services/metrics/dashboard/pod_dashboard_service.rb new file mode 100644 index 00000000000..16b87d2d587 --- /dev/null +++ b/app/services/metrics/dashboard/pod_dashboard_service.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Metrics + module Dashboard + class PodDashboardService < ::Metrics::Dashboard::PredefinedDashboardService + DASHBOARD_PATH = 'config/prometheus/pod_metrics.yml' + DASHBOARD_NAME = 'Pod Health' + end + end +end diff --git a/app/services/metrics/dashboard/predefined_dashboard_service.rb b/app/services/metrics/dashboard/predefined_dashboard_service.rb new file mode 100644 index 00000000000..1be1a000854 --- /dev/null +++ b/app/services/metrics/dashboard/predefined_dashboard_service.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Metrics + module Dashboard + class PredefinedDashboardService < ::Metrics::Dashboard::BaseService + # These constants should be overridden in the inheriting class. For Ex: + # DASHBOARD_PATH = 'config/prometheus/common_metrics.yml' + # DASHBOARD_NAME = 'Default' + DASHBOARD_PATH = nil + DASHBOARD_NAME = nil + + SEQUENCE = [ + STAGES::EndpointInserter, + STAGES::Sorter + ].freeze + + class << self + def matching_dashboard?(filepath) + filepath == self::DASHBOARD_PATH + end + end + + private + + def cache_key + "metrics_dashboard_#{dashboard_path}" + end + + def dashboard_path + self.class::DASHBOARD_PATH + end + + # Returns the base metrics shipped with every GitLab service. + def get_raw_dashboard + yml = File.read(Rails.root.join(dashboard_path)) + + YAML.safe_load(yml) + end + + def sequence + self.class::SEQUENCE + end + end + end +end diff --git a/app/services/metrics/dashboard/system_dashboard_service.rb b/app/services/metrics/dashboard/system_dashboard_service.rb index f8dbb8a705c..bef65dbe1c2 100644 --- a/app/services/metrics/dashboard/system_dashboard_service.rb +++ b/app/services/metrics/dashboard/system_dashboard_service.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true # Fetches the system metrics dashboard and formats the output. -# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards. +# Use Gitlab::Metrics::Dashboard::Finder to retrieve dashboards. module Metrics module Dashboard - class SystemDashboardService < ::Metrics::Dashboard::BaseService - SYSTEM_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml' - SYSTEM_DASHBOARD_NAME = 'Default' + class SystemDashboardService < ::Metrics::Dashboard::PredefinedDashboardService + DASHBOARD_PATH = 'config/prometheus/common_metrics.yml' + DASHBOARD_NAME = 'Default' SEQUENCE = [ STAGES::CommonMetricsInserter, @@ -18,37 +18,12 @@ module Metrics class << self def all_dashboard_paths(_project) [{ - path: SYSTEM_DASHBOARD_PATH, - display_name: SYSTEM_DASHBOARD_NAME, + path: DASHBOARD_PATH, + display_name: DASHBOARD_NAME, default: true, system_dashboard: true }] end - - def system_dashboard?(filepath) - filepath == SYSTEM_DASHBOARD_PATH - end - end - - private - - def cache_key - "metrics_dashboard_#{dashboard_path}" - end - - def dashboard_path - SYSTEM_DASHBOARD_PATH - end - - # Returns the base metrics shipped with every GitLab service. - def get_raw_dashboard - yml = File.read(Rails.root.join(dashboard_path)) - - YAML.safe_load(yml) - end - - def sequence - SEQUENCE end end end diff --git a/app/services/metrics/sample_metrics_service.rb b/app/services/metrics/sample_metrics_service.rb new file mode 100644 index 00000000000..719bc6614e4 --- /dev/null +++ b/app/services/metrics/sample_metrics_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Metrics + class SampleMetricsService + DIRECTORY = "sample_metrics" + + attr_reader :identifier + + def initialize(identifier) + @identifier = identifier + end + + def query + return unless identifier && File.exist?(file_location) + + YAML.load_file(File.expand_path(file_location, __dir__)) + end + + private + + def file_location + sanitized_string = identifier.gsub(/[^0-9A-Za-z_]/, '') + File.join(Rails.root, DIRECTORY, "#{sanitized_string}.yml") + end + end +end diff --git a/app/services/notes/base_service.rb b/app/services/notes/base_service.rb index b4d04c47cc0..87f7cb0e8ac 100644 --- a/app/services/notes/base_service.rb +++ b/app/services/notes/base_service.rb @@ -4,7 +4,7 @@ module Notes class BaseService < ::BaseService def clear_noteable_diffs_cache(note) if note.is_a?(DiffNote) && - note.discussion_first_note? && + note.start_of_discussion? && note.position.unfolded_diff?(project.repository) note.noteable.diffs.clear_cache end diff --git a/app/services/notes/build_service.rb b/app/services/notes/build_service.rb index 541f3e0d23c..cf21818a886 100644 --- a/app/services/notes/build_service.rb +++ b/app/services/notes/build_service.rb @@ -11,7 +11,7 @@ module Notes unless discussion && can?(current_user, :create_note, discussion.noteable) note = Note.new - note.errors.add(:base, 'Discussion to reply to cannot be found') + note.errors.add(:base, _('Discussion to reply to cannot be found')) return note end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 9e6cbfa06fe..accfdb5b863 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -2,6 +2,7 @@ module Notes class CreateService < ::Notes::BaseService + # rubocop:disable Metrics/CyclomaticComplexity def execute merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha) @@ -9,7 +10,9 @@ module Notes # n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/37440 note_valid = Gitlab::GitalyClient.allow_n_plus_1_calls do - note.valid? + # We may set errors manually in Notes::BuildService for this reason + # we also need to check for already existing errors. + note.errors.empty? && note.valid? end return note unless note_valid @@ -33,7 +36,11 @@ module Notes NewNoteWorker.perform_async(note.id) end - if !only_commands && note.save + note_saved = note.with_transaction_returning_status do + !only_commands && note.save && note.store_mentions! + end + + if note_saved if note.part_of_discussion? && note.discussion.can_convert_to_discussion? note.discussion.convert_to_discussion!(save: true) end @@ -63,6 +70,7 @@ module Notes note end + # rubocop:enable Metrics/CyclomaticComplexity private diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index 573be8fbe8b..15c556498ec 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -7,7 +7,11 @@ module Notes old_mentioned_users = note.mentioned_users(current_user).to_a - note.update(params.merge(updated_by: current_user)) + note.assign_attributes(params.merge(updated_by: current_user)) + + note.with_transaction_returning_status do + note.save && note.store_mentions! + end only_commands = false diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 1709474a6c7..a75eaa99c23 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -58,6 +58,14 @@ class NotificationService end end + # Notify the owner of the personal access token, when it is about to expire + # And mark the token with about_to_expire_delivered + def access_token_about_to_expire(user) + return unless user.can?(:receive_notifications) + + mailer.access_token_about_to_expire_email(user).deliver_later + end + # When create an issue we should send an email to: # # * issue assignee if their notification level is not Disabled diff --git a/app/services/pages/delete_service.rb b/app/services/pages/delete_service.rb new file mode 100644 index 00000000000..d4de6bb750d --- /dev/null +++ b/app/services/pages/delete_service.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Pages + class DeleteService < BaseService + def execute + project.remove_pages + project.pages_domains.destroy_all # rubocop: disable DestroyAll + 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 1b880a7aab1..b995df12e56 100644 --- a/app/services/projects/container_repository/cleanup_tags_service.rb +++ b/app/services/projects/container_repository/cleanup_tags_service.rb @@ -26,13 +26,13 @@ module Projects def delete_tags(tags_to_delete, tags_by_digest) deleted_digests = group_by_digest(tags_to_delete).select do |digest, tags| - delete_tag_digest(digest, tags, tags_by_digest[digest]) + delete_tag_digest(tags, tags_by_digest[digest]) end deleted_digests.values.flatten end - def delete_tag_digest(digest, tags, other_tags) + def delete_tag_digest(tags, other_tags) # Issue: https://gitlab.com/gitlab-org/gitlab-foss/issues/21405 # we have to remove all tags due # to Docker Distribution bug unable diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb index 48bd9394dc5..88ff3c2c9df 100644 --- a/app/services/projects/container_repository/delete_tags_service.rb +++ b/app/services/projects/container_repository/delete_tags_service.rb @@ -24,32 +24,36 @@ module Projects dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path) return error('could not generate manifest') if dummy_manifest.nil? - # update the manifests of the tags with the new dummy image - deleted_tags = [] - tag_digests = [] + deleted_tags = replace_tag_manifests(container_repository, dummy_manifest, tag_names) + + # Deletes the dummy image + # All created tag digests are the same since they all have the same dummy image. + # a single delete is sufficient to remove all tags with it + if deleted_tags.any? && container_repository.delete_tag_by_digest(deleted_tags.values.first) + success(deleted: deleted_tags.keys) + else + error('could not delete tags') + end + end + + # update the manifests of the tags with the new dummy image + def replace_tag_manifests(container_repository, dummy_manifest, tag_names) + deleted_tags = {} tag_names.each do |name| digest = container_repository.client.put_tag(container_repository.path, name, dummy_manifest) next unless digest - deleted_tags << name - tag_digests << digest + deleted_tags[name] = digest end # make sure the digests are the same (it should always be) - tag_digests.uniq! + digests = deleted_tags.values.uniq # rubocop: disable CodeReuse/ActiveRecord - Gitlab::Sentry.track_exception(ArgumentError.new('multiple tag digests')) if tag_digests.many? + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new('multiple tag digests')) if digests.many? - # Deletes the dummy image - # All created tag digests are the same since they all have the same dummy image. - # a single delete is sufficient to remove all tags with it - if tag_digests.any? && container_repository.delete_tag_by_digest(tag_digests.first) - success(deleted: deleted_tags) - else - error('could not delete tags') - end + deleted_tags end end end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 90e703e7050..cbed794f92e 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -31,13 +31,6 @@ module Projects Projects::UnlinkForkService.new(project, current_user).execute - # The project is not necessarily a fork, so update the fork network originating - # from this project - if fork_network = project.root_of_fork_network - fork_network.update(root_project: nil, - deleted_root_project_name: project.full_name) - end - attempt_destroy_transaction(project) system_hook_service.execute_hooks_for(project, :destroy) diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 47ab7f9a8a0..e66a0ed181a 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -3,11 +3,16 @@ module Projects class ForkService < BaseService def execute(fork_to_project = nil) - if fork_to_project - link_existing_project(fork_to_project) - else - fork_new_project - end + forked_project = + if fork_to_project + link_existing_project(fork_to_project) + else + fork_new_project + end + + refresh_forks_count if forked_project&.saved? + + forked_project end private @@ -92,8 +97,7 @@ module Projects def link_fork_network(fork_to_project) return if fork_to_project.errors.any? - fork_to_project.fork_network_member.save && - refresh_forks_count + fork_to_project.fork_network_member.save end def refresh_forks_count diff --git a/app/services/projects/hashed_storage/base_repository_service.rb b/app/services/projects/hashed_storage/base_repository_service.rb index 8b1bcaf17b7..09de8d9f0da 100644 --- a/app/services/projects/hashed_storage/base_repository_service.rb +++ b/app/services/projects/hashed_storage/base_repository_service.rb @@ -8,13 +8,12 @@ module Projects class BaseRepositoryService < BaseService include Gitlab::ShellAdapter - attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version, :logger, :move_wiki + attr_reader :old_disk_path, :new_disk_path, :old_storage_version, :logger, :move_wiki def initialize(project:, old_disk_path:, logger: nil) @project = project @logger = logger || Gitlab::AppLogger @old_disk_path = old_disk_path - @old_wiki_disk_path = "#{old_disk_path}.wiki" @move_wiki = has_wiki? end @@ -44,9 +43,21 @@ module Projects gitlab_shell.mv_repository(project.repository_storage, from_name, to_name) end + def move_repositories + result = move_repository(old_disk_path, new_disk_path) + project.reload_repository! + + if move_wiki + result &&= move_repository(old_wiki_disk_path, new_wiki_disk_path) + project.clear_memoization(:wiki) + end + + result + end + def rollback_folder_move move_repository(new_disk_path, old_disk_path) - move_repository("#{new_disk_path}.wiki", old_wiki_disk_path) + move_repository(new_wiki_disk_path, old_wiki_disk_path) end def try_to_set_repository_read_only! @@ -58,6 +69,20 @@ module Projects raise RepositoryInUseError, migration_error end end + + def wiki_path_suffix + @wiki_path_suffix ||= Gitlab::GlRepository::WIKI.path_suffix + end + + def old_wiki_disk_path + @old_wiki_disk_path ||= "#{old_disk_path}#{wiki_path_suffix}" + end + + def new_wiki_disk_path + @new_wiki_disk_path ||= "#{new_disk_path}#{wiki_path_suffix}" + end end end end + +Projects::HashedStorage::BaseRepositoryService.prepend_if_ee('EE::Projects::HashedStorage::BaseRepositoryService') diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb index 0a0bd90cd20..fd62ac37d27 100644 --- a/app/services/projects/hashed_storage/migrate_repository_service.rb +++ b/app/services/projects/hashed_storage/migrate_repository_service.rb @@ -11,11 +11,7 @@ module Projects @new_disk_path = project.disk_path - result = move_repository(old_disk_path, new_disk_path) - - if move_wiki - result &&= move_repository(old_wiki_disk_path, "#{new_disk_path}.wiki") - end + result = move_repositories if result project.write_repository_config diff --git a/app/services/projects/hashed_storage/rollback_repository_service.rb b/app/services/projects/hashed_storage/rollback_repository_service.rb index a705112ebe3..d6646e3765e 100644 --- a/app/services/projects/hashed_storage/rollback_repository_service.rb +++ b/app/services/projects/hashed_storage/rollback_repository_service.rb @@ -11,11 +11,7 @@ module Projects @new_disk_path = project.disk_path - result = move_repository(old_disk_path, new_disk_path) - - if move_wiki - result &&= move_repository(old_wiki_disk_path, "#{new_disk_path}.wiki") - end + result = move_repositories if result project.write_repository_config diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 073c14040ce..cc12aacaf02 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -25,13 +25,13 @@ module Projects success rescue Gitlab::UrlBlocker::BlockedUrlError => e - Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type }) + Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path, importer: project.import_type) error(s_("ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}") % { project_safe_import_url: project.safe_import_url, project_full_path: project.full_path, message: e.message }) rescue => e message = Projects::ImportErrorFilter.filter_message(e.message) - Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type }) + Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path, importer: project.import_type) error(s_("ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}") % { project_safe_import_url: project.safe_import_url, project_full_path: project.full_path, message: message }) end diff --git a/app/services/projects/overwrite_project_service.rb b/app/services/projects/overwrite_project_service.rb index 696e1b665b2..c5e38f166da 100644 --- a/app/services/projects/overwrite_project_service.rb +++ b/app/services/projects/overwrite_project_service.rb @@ -7,7 +7,9 @@ module Projects Project.transaction do move_before_destroy_relationships(source_project) - destroy_old_project(source_project) + # Reset is required in order to get the proper + # uncached fork network method calls value. + destroy_old_project(source_project.reset) rename_project(source_project.name, source_project.path) @project diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index 1b8a920268f..e7e0141099e 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -2,34 +2,67 @@ module Projects class UnlinkForkService < BaseService - # rubocop: disable CodeReuse/ActiveRecord + # If a fork is given, it: + # + # - Saves LFS objects to the root project + # - Close existing MRs coming from it + # - Is removed from the fork network + # + # If a root of fork(s) is given, it does the same, + # but not updating LFS objects (there'll be no related root to cache it). def execute - return unless @project.forked? + fork_network = @project.fork_network - if fork_source = @project.fork_source - fork_source.lfs_objects.find_each do |lfs_object| - lfs_object.projects << @project unless lfs_object.projects.include?(@project) - end + return unless fork_network - refresh_forks_count(fork_source) - end + save_lfs_objects - merge_requests = @project.fork_network + merge_requests = fork_network .merge_requests .opened - .where.not(target_project: @project) - .from_project(@project) + .from_and_to_forks(@project) - merge_requests.each do |mr| + merge_requests.find_each do |mr| ::MergeRequests::CloseService.new(@project, @current_user).execute(mr) end - @project.fork_network_member.destroy + Project.transaction do + # Get out of the fork network as a member and + # remove references from all its direct forks. + @project.fork_network_member.destroy + @project.forked_to_members.update_all(forked_from_project_id: nil) + + # The project is not necessarily a fork, so update the fork network originating + # from this project + if fork_network = @project.root_of_fork_network + fork_network.update(root_project: nil, deleted_root_project_name: @project.full_name) + end + end + + # When the project getting out of the network is a node with parent + # and children, both the parent and the node needs a cache refresh. + [@project.forked_from_project, @project].compact.each do |project| + refresh_forks_count(project) + end end - # rubocop: enable CodeReuse/ActiveRecord + + private def refresh_forks_count(project) Projects::ForksCountService.new(project).refresh_cache end + + def save_lfs_objects + return unless @project.forked? + + lfs_storage_project = @project.lfs_storage_project + + return unless lfs_storage_project + return if lfs_storage_project == @project # that project is being unlinked + + lfs_storage_project.lfs_objects.find_each do |lfs_object| + lfs_object.projects << @project unless lfs_object.projects.include?(@project) + end + end end end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 2dad1d05a2c..aedd7252f63 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -65,7 +65,7 @@ module Projects ) project_changed_feature_keys = project.project_feature.previous_changes.keys - if project.previous_changes.include?(:visibility_level) && project.private? + if project.visibility_level_previous_changes && project.private? # don't enqueue immediately to prevent todos removal in case of a mistake TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id) TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id) @@ -79,6 +79,11 @@ module Projects system_hook_service.execute_hooks_for(project, :update) end + if project.visibility_level_decreased? && project.unlink_forks_upon_visibility_decrease_enabled? + # It's a system-bounded operation, so no extra authorization check is required. + Projects::UnlinkForkService.new(project, current_user).execute + end + update_pages_config if changing_pages_related_config? end diff --git a/app/services/prometheus/proxy_variable_substitution_service.rb b/app/services/prometheus/proxy_variable_substitution_service.rb new file mode 100644 index 00000000000..ca56292e9d6 --- /dev/null +++ b/app/services/prometheus/proxy_variable_substitution_service.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Prometheus + class ProxyVariableSubstitutionService < BaseService + include Stepable + + steps :add_params_to_result, :substitute_ruby_variables + + def initialize(environment, params = {}) + @environment, @params = environment, params.deep_dup + end + + def execute + execute_steps + end + + private + + def add_params_to_result(result) + result[:params] = params + + success(result) + end + + def substitute_ruby_variables(result) + return success(result) unless query + + # The % operator doesn't replace variables if the hash contains string + # keys. + result[:params][:query] = query % predefined_context.symbolize_keys + + success(result) + rescue TypeError, ArgumentError => exception + log_error(exception.message) + Gitlab::ErrorTracking.track_exception(exception, extra: { + template_string: query, + variables: predefined_context + }) + + error(_('Malformed string')) + end + + def predefined_context + @predefined_context ||= Gitlab::Prometheus::QueryVariables.call(@environment) + end + + def query + params[:query] + end + end +end diff --git a/app/services/repair_ldap_blocked_user_service.rb b/app/services/repair_ldap_blocked_user_service.rb deleted file mode 100644 index 6ed42054ac3..00000000000 --- a/app/services/repair_ldap_blocked_user_service.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -class RepairLdapBlockedUserService - attr_accessor :user - - def initialize(user) - @user = user - end - - def execute - user.block if ldap_hard_blocked? - end - - private - - def ldap_hard_blocked? - user.ldap_blocked? && !user.ldap_user? - end -end diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb index 415a02ab337..7927ab265c5 100644 --- a/app/services/submit_usage_ping_service.rb +++ b/app/services/submit_usage_ping_service.rb @@ -38,7 +38,7 @@ class SubmitUsagePingService def store_metrics(response) return unless response['conv_index'].present? - ConversationalDevelopmentIndex::Metric.create!( + DevOpsScore::Metric.create!( response['conv_index'].slice(*METRICS) ) end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 2299a02fea1..55f888d5664 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -174,6 +174,19 @@ class TodoService mark_todos_as_done(todos, current_user) end + def mark_all_todos_as_done_by_user(current_user) + todos = TodosFinder.new(current_user).execute + mark_todos_as_done(todos, current_user) + end + + def mark_todo_as_done(todo, current_user) + return if todo.done? + + todo.update(state: :done) + + current_user.update_todos_count_cache + end + # When user marks some todos as pending def mark_todos_as_pending(todos, current_user) update_todos_state(todos, current_user, :pending) diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb index a294812ef9e..ac7f8e9b1f5 100644 --- a/app/services/update_snippet_service.rb +++ b/app/services/update_snippet_service.rb @@ -25,8 +25,12 @@ class UpdateSnippetService < BaseService snippet.assign_attributes(params) spam_check(snippet, current_user) - snippet.save.tap do |succeeded| - Gitlab::UsageDataCounters::SnippetCounter.count(:update) if succeeded + snippet_saved = snippet.with_transaction_returning_status do + snippet.save && snippet.store_mentions! + end + + if snippet_saved + Gitlab::UsageDataCounters::SnippetCounter.count(:update) end end end diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index 8c85ad9ffd8..ea4d11e728e 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -23,7 +23,7 @@ module Users @reset_token = user.generate_reset_token if params[:reset_password] if user_params[:force_random_password] - random_password = Devise.friendly_token.first(Devise.password_length.min) + random_password = Devise.friendly_token.first(User.password_length.min) user.password = user.password_confirmation = random_password end end diff --git a/app/services/users/repair_ldap_blocked_service.rb b/app/services/users/repair_ldap_blocked_service.rb new file mode 100644 index 00000000000..378145a65b3 --- /dev/null +++ b/app/services/users/repair_ldap_blocked_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Users + class RepairLdapBlockedService + attr_accessor :user + + def initialize(user) + @user = user + end + + def execute + user.block if ldap_hard_blocked? + end + + private + + def ldap_hard_blocked? + user.ldap_blocked? && !user.ldap_user? + end + end +end diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb deleted file mode 100644 index 3f4a59e5cee..00000000000 --- a/app/services/validate_new_branch_service.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require_relative 'base_service' - -class ValidateNewBranchService < BaseService - def execute(branch_name, force: false) - valid_branch = Gitlab::GitRefValidator.validate(branch_name) - - unless valid_branch - return error('Branch name is invalid') - end - - if project.repository.branch_exists?(branch_name) && !force - return error('Branch already exists') - end - - success - rescue Gitlab::Git::PreReceiveError => ex - error(ex.message) - end -end diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index 8c294218708..87edac36e33 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -92,9 +92,6 @@ class WebHookService end def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil) - # logging for ServiceHook's is not available - return if hook.is_a?(ServiceHook) - WebHookLog.create( web_hook: hook, trigger: trigger, |