diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-17 11:59:07 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-17 11:59:07 +0000 |
commit | 8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch) | |
tree | 544930fb309b30317ae9797a9683768705d664c4 /lib | |
parent | 4b1de649d0168371549608993deac953eb692019 (diff) | |
download | gitlab-ce-8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca.tar.gz |
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'lib')
346 files changed, 6233 insertions, 2948 deletions
diff --git a/lib/api/admin/instance_clusters.rb b/lib/api/admin/instance_clusters.rb index 679e231b283..b724d3a38dc 100644 --- a/lib/api/admin/instance_clusters.rb +++ b/lib/api/admin/instance_clusters.rb @@ -76,6 +76,7 @@ module API optional :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace' optional :domain, type: String, desc: 'Cluster base domain' optional :management_project_id, type: Integer, desc: 'The ID of the management project' + optional :managed, type: Boolean, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster' optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do optional :api_url, type: String, desc: 'URL to access the Kubernetes API' optional :token, type: String, desc: 'Token to authenticate against Kubernetes' diff --git a/lib/api/api.rb b/lib/api/api.rb index ea149f25584..06c2b46a2f2 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -211,7 +211,7 @@ module API mount ::API::ProjectPackages mount ::API::GroupPackages mount ::API::PackageFiles - mount ::API::NugetPackages + mount ::API::NugetProjectPackages mount ::API::PypiPackages mount ::API::ComposerPackages mount ::API::ConanProjectPackages diff --git a/lib/api/boards.rb b/lib/api/boards.rb index f4b23c507f4..e2d30dd7c2b 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -117,8 +117,6 @@ module API use :list_creation_params end post '/lists' do - authorize_list_type_resource! - authorize!(:admin_list, user_project) create_list diff --git a/lib/api/boards_responses.rb b/lib/api/boards_responses.rb index 2ae82f78e01..89355c84401 100644 --- a/lib/api/boards_responses.rb +++ b/lib/api/boards_responses.rb @@ -45,21 +45,17 @@ module API def create_list create_list_service = - ::Boards::Lists::CreateService.new(board_parent, current_user, create_list_params) + ::Boards::Lists::CreateService.new(board_parent, current_user, declared_params.compact.with_indifferent_access) - list = create_list_service.execute(board) + response = create_list_service.execute(board) - if list.valid? - present list, with: Entities::List + if response.success? + present response.payload[:list], with: Entities::List else - render_validation_error!(list) + render_api_error!({ error: response.errors.first }, 400) end end - def create_list_params - params.slice(:label_id) - end - def move_list(list) move_list_service = ::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i }) @@ -80,14 +76,6 @@ module API end end - # rubocop: disable CodeReuse/ActiveRecord - def authorize_list_type_resource! - unless available_labels_for(board_parent).exists?(params[:label_id]) - render_api_error!({ error: 'Label not found!' }, 400) - end - end - # rubocop: enable CodeReuse/ActiveRecord - params :list_creation_params do requires :label_id, type: Integer, desc: 'The ID of an existing label' end diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 85232b4ae1b..86e1a939df1 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -176,6 +176,10 @@ module API optional :state, type: String, desc: %q(Job's status: success, failed) optional :checksum, type: String, desc: %q(Job's trace CRC32 checksum) optional :failure_reason, type: String, desc: %q(Job's failure_reason) + optional :output, type: Hash, desc: %q(Build log state) do + optional :checksum, type: String, desc: %q(Job's trace CRC32 checksum) + optional :bytesize, type: Integer, desc: %q(Job's trace size in bytes) + end end put '/:id' do job = authenticate_job! diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb index 0ac5cc45ccf..1181650fe96 100644 --- a/lib/api/composer_packages.rb +++ b/lib/api/composer_packages.rb @@ -38,6 +38,8 @@ module API packages = ::Packages::Composer::PackagesFinder.new(current_user, user_group).execute if params[:package_name].present? + params[:package_name], params[:sha] = params[:package_name].split('$') + packages = packages.with_name(params[:package_name]) end @@ -93,6 +95,7 @@ module API get ':id/-/packages/composer/*package_name', requirements: COMPOSER_ENDPOINT_REQUIREMENTS, file_path: true do not_found! if packages.empty? + not_found! if params[:sha].blank? presenter.package_versions end @@ -132,7 +135,7 @@ module API track_package_event('push_package', :composer) ::Packages::Composer::CreatePackageService - .new(authorized_user_project, current_user, declared_params) + .new(authorized_user_project, current_user, declared_params.merge(build: current_authenticated_job)) .execute created! diff --git a/lib/api/conan_instance_packages.rb b/lib/api/conan_instance_packages.rb index 08265201328..8c13b580092 100644 --- a/lib/api/conan_instance_packages.rb +++ b/lib/api/conan_instance_packages.rb @@ -4,7 +4,7 @@ module API class ConanInstancePackages < ::API::Base namespace 'packages/conan/v1' do - include ConanPackageEndpoints + include ::API::Concerns::Packages::ConanEndpoints end end end diff --git a/lib/api/conan_package_endpoints.rb b/lib/api/conan_package_endpoints.rb deleted file mode 100644 index 188a42f26f8..00000000000 --- a/lib/api/conan_package_endpoints.rb +++ /dev/null @@ -1,351 +0,0 @@ -# frozen_string_literal: true - -# Conan Package Manager Client API -# -# These API endpoints are not consumed directly by users, so there is no documentation for the -# individual endpoints. They are called by the Conan package manager client when users run commands -# like `conan install` or `conan upload`. The usage of the GitLab Conan repository is documented here: -# https://docs.gitlab.com/ee/user/packages/conan_repository/#installing-a-package -# -# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798 -module API - module ConanPackageEndpoints - extend ActiveSupport::Concern - - PACKAGE_REQUIREMENTS = { - package_name: API::NO_SLASH_URL_PART_REGEX, - package_version: API::NO_SLASH_URL_PART_REGEX, - package_username: API::NO_SLASH_URL_PART_REGEX, - package_channel: API::NO_SLASH_URL_PART_REGEX - }.freeze - - FILE_NAME_REQUIREMENTS = { - file_name: API::NO_SLASH_URL_PART_REGEX - }.freeze - - PACKAGE_COMPONENT_REGEX = Gitlab::Regex.conan_recipe_component_regex - CONAN_REVISION_REGEX = Gitlab::Regex.conan_revision_regex - - CONAN_FILES = (Gitlab::Regex::Packages::CONAN_RECIPE_FILES + Gitlab::Regex::Packages::CONAN_PACKAGE_FILES).freeze - - included do - feature_category :package_registry - - helpers ::API::Helpers::PackagesManagerClientsHelpers - helpers ::API::Helpers::Packages::Conan::ApiHelpers - helpers ::API::Helpers::RelatedResourcesHelpers - - before do - require_packages_enabled! - - # Personal access token will be extracted from Bearer or Basic authorization - # in the overridden find_personal_access_token or find_user_from_job_token helpers - authenticate! - end - - desc 'Ping the Conan API' do - detail 'This feature was introduced in GitLab 12.2' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get 'ping' do - header 'X-Conan-Server-Capabilities', [].join(',') - end - - desc 'Search for packages' do - detail 'This feature was introduced in GitLab 12.4' - end - - params do - requires :q, type: String, desc: 'Search query' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get 'conans/search' do - service = ::Packages::Conan::SearchService.new(current_user, query: params[:q]).execute - service.payload - end - - namespace 'users' do - format :txt - - desc 'Authenticate user against conan CLI' do - detail 'This feature was introduced in GitLab 12.2' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get 'authenticate' do - unauthorized! unless token - - token.to_jwt - end - - desc 'Check for valid user credentials per conan CLI' do - detail 'This feature was introduced in GitLab 12.4' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get 'check_credentials' do - authenticate! - :ok - end - end - - params do - requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name' - requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version' - requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username' - requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel' - end - namespace 'conans/:package_name/:package_version/:package_username/:package_channel', requirements: PACKAGE_REQUIREMENTS do - # Get the snapshot - # - # the snapshot is a hash of { filename: md5 hash } - # md5 hash is the has of that file. This hash is used to diff the files existing on the client - # to determine which client files need to be uploaded if no recipe exists the snapshot is empty - desc 'Package Snapshot' do - detail 'This feature was introduced in GitLab 12.5' - end - - params do - requires :conan_package_reference, type: String, desc: 'Conan package ID' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get 'packages/:conan_package_reference' do - authorize!(:read_package, project) - - presenter = ::Packages::Conan::PackagePresenter.new( - package, - current_user, - project, - conan_package_reference: params[:conan_package_reference] - ) - - present presenter, with: ::API::Entities::ConanPackage::ConanPackageSnapshot - end - - desc 'Recipe Snapshot' do - detail 'This feature was introduced in GitLab 12.5' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get do - authorize!(:read_package, project) - - presenter = ::Packages::Conan::PackagePresenter.new(package, current_user, project) - - present presenter, with: ::API::Entities::ConanPackage::ConanRecipeSnapshot - end - - # Get the manifest - # returns the download urls for the existing recipe in the registry - # - # the manifest is a hash of { filename: url } - # where the url is the download url for the file - desc 'Package Digest' do - detail 'This feature was introduced in GitLab 12.5' - end - params do - requires :conan_package_reference, type: String, desc: 'Conan package ID' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get 'packages/:conan_package_reference/digest' do - present_package_download_urls - end - - desc 'Recipe Digest' do - detail 'This feature was introduced in GitLab 12.5' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get 'digest' do - present_recipe_download_urls - end - - # Get the download urls - # - # returns the download urls for the existing recipe or package in the registry - # - # the manifest is a hash of { filename: url } - # where the url is the download url for the file - desc 'Package Download Urls' do - detail 'This feature was introduced in GitLab 12.5' - end - - params do - requires :conan_package_reference, type: String, desc: 'Conan package ID' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get 'packages/:conan_package_reference/download_urls' do - present_package_download_urls - end - - desc 'Recipe Download Urls' do - detail 'This feature was introduced in GitLab 12.5' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get 'download_urls' do - present_recipe_download_urls - end - - # Get the upload urls - # - # request body contains { filename: filesize } where the filename is the - # name of the file the conan client is requesting to upload - # - # returns { filename: url } - # where the url is the upload url for the file that the conan client will use - desc 'Package Upload Urls' do - detail 'This feature was introduced in GitLab 12.4' - end - - params do - requires :conan_package_reference, type: String, desc: 'Conan package ID' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - post 'packages/:conan_package_reference/upload_urls' do - authorize!(:read_package, project) - - status 200 - present package_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls - end - - desc 'Recipe Upload Urls' do - detail 'This feature was introduced in GitLab 12.4' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - post 'upload_urls' do - authorize!(:read_package, project) - - status 200 - present recipe_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls - end - - desc 'Delete Package' do - detail 'This feature was introduced in GitLab 12.5' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - delete do - authorize!(:destroy_package, project) - - track_package_event('delete_package', :conan, category: 'API::ConanPackages') - - package.destroy - end - end - - params do - requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name' - requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version' - requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username' - requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel' - requires :recipe_revision, type: String, regexp: CONAN_REVISION_REGEX, desc: 'Conan Recipe Revision' - end - namespace 'files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision', requirements: PACKAGE_REQUIREMENTS do - before do - authenticate_non_get! - end - - params do - requires :file_name, type: String, desc: 'Package file name', values: CONAN_FILES - end - namespace 'export/:file_name', requirements: FILE_NAME_REQUIREMENTS do - desc 'Download recipe files' do - detail 'This feature was introduced in GitLab 12.6' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get do - download_package_file(:recipe_file) - end - - desc 'Upload recipe package files' do - detail 'This feature was introduced in GitLab 12.6' - end - - params do - requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - put do - upload_package_file(:recipe_file) - end - - desc 'Workhorse authorize the conan recipe file' do - detail 'This feature was introduced in GitLab 12.6' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - put 'authorize' do - authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size) - end - end - - params do - requires :conan_package_reference, type: String, desc: 'Conan Package ID' - requires :package_revision, type: String, desc: 'Conan Package Revision' - requires :file_name, type: String, desc: 'Package file name', values: CONAN_FILES - end - namespace 'package/:conan_package_reference/:package_revision/:file_name', requirements: FILE_NAME_REQUIREMENTS do - desc 'Download package files' do - detail 'This feature was introduced in GitLab 12.5' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - get do - download_package_file(:package_file) - end - - desc 'Workhorse authorize the conan package file' do - detail 'This feature was introduced in GitLab 12.6' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - put 'authorize' do - authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size) - end - - desc 'Upload package files' do - detail 'This feature was introduced in GitLab 12.6' - end - - params do - requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' - end - - route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true - - put do - upload_package_file(:package_file) - end - end - end - end - end -end diff --git a/lib/api/conan_project_packages.rb b/lib/api/conan_project_packages.rb index db8cd187811..636b5dca5ed 100644 --- a/lib/api/conan_project_packages.rb +++ b/lib/api/conan_project_packages.rb @@ -9,7 +9,7 @@ module API resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do namespace ':id/packages/conan/v1' do - include ConanPackageEndpoints + include ::API::Concerns::Packages::ConanEndpoints end end end diff --git a/lib/api/concerns/packages/conan_endpoints.rb b/lib/api/concerns/packages/conan_endpoints.rb new file mode 100644 index 00000000000..6c8b3a1ba4a --- /dev/null +++ b/lib/api/concerns/packages/conan_endpoints.rb @@ -0,0 +1,355 @@ +# frozen_string_literal: true + +# Conan Package Manager Client API +# +# These API endpoints are not consumed directly by users, so there is no documentation for the +# individual endpoints. They are called by the Conan package manager client when users run commands +# like `conan install` or `conan upload`. The usage of the GitLab Conan repository is documented here: +# https://docs.gitlab.com/ee/user/packages/conan_repository/#installing-a-package +# +# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798 +module API + module Concerns + module Packages + module ConanEndpoints + extend ActiveSupport::Concern + + PACKAGE_REQUIREMENTS = { + package_name: API::NO_SLASH_URL_PART_REGEX, + package_version: API::NO_SLASH_URL_PART_REGEX, + package_username: API::NO_SLASH_URL_PART_REGEX, + package_channel: API::NO_SLASH_URL_PART_REGEX + }.freeze + + FILE_NAME_REQUIREMENTS = { + file_name: API::NO_SLASH_URL_PART_REGEX + }.freeze + + PACKAGE_COMPONENT_REGEX = Gitlab::Regex.conan_recipe_component_regex + CONAN_REVISION_REGEX = Gitlab::Regex.conan_revision_regex + + CONAN_FILES = (Gitlab::Regex::Packages::CONAN_RECIPE_FILES + Gitlab::Regex::Packages::CONAN_PACKAGE_FILES).freeze + + included do + feature_category :package_registry + + helpers ::API::Helpers::PackagesManagerClientsHelpers + helpers ::API::Helpers::Packages::Conan::ApiHelpers + helpers ::API::Helpers::RelatedResourcesHelpers + + before do + require_packages_enabled! + + # Personal access token will be extracted from Bearer or Basic authorization + # in the overridden find_personal_access_token or find_user_from_job_token helpers + authenticate! + end + + desc 'Ping the Conan API' do + detail 'This feature was introduced in GitLab 12.2' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get 'ping' do + header 'X-Conan-Server-Capabilities', [].join(',') + end + + desc 'Search for packages' do + detail 'This feature was introduced in GitLab 12.4' + end + + params do + requires :q, type: String, desc: 'Search query' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get 'conans/search' do + service = ::Packages::Conan::SearchService.new(current_user, query: params[:q]).execute + service.payload + end + + namespace 'users' do + format :txt + + desc 'Authenticate user against conan CLI' do + detail 'This feature was introduced in GitLab 12.2' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get 'authenticate' do + unauthorized! unless token + + token.to_jwt + end + + desc 'Check for valid user credentials per conan CLI' do + detail 'This feature was introduced in GitLab 12.4' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get 'check_credentials' do + authenticate! + :ok + end + end + + params do + requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name' + requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version' + requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username' + requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel' + end + namespace 'conans/:package_name/:package_version/:package_username/:package_channel', requirements: PACKAGE_REQUIREMENTS do + # Get the snapshot + # + # the snapshot is a hash of { filename: md5 hash } + # md5 hash is the has of that file. This hash is used to diff the files existing on the client + # to determine which client files need to be uploaded if no recipe exists the snapshot is empty + desc 'Package Snapshot' do + detail 'This feature was introduced in GitLab 12.5' + end + + params do + requires :conan_package_reference, type: String, desc: 'Conan package ID' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get 'packages/:conan_package_reference' do + authorize!(:read_package, project) + + presenter = ::Packages::Conan::PackagePresenter.new( + package, + current_user, + project, + conan_package_reference: params[:conan_package_reference] + ) + + present presenter, with: ::API::Entities::ConanPackage::ConanPackageSnapshot + end + + desc 'Recipe Snapshot' do + detail 'This feature was introduced in GitLab 12.5' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get do + authorize!(:read_package, project) + + presenter = ::Packages::Conan::PackagePresenter.new(package, current_user, project) + + present presenter, with: ::API::Entities::ConanPackage::ConanRecipeSnapshot + end + + # Get the manifest + # returns the download urls for the existing recipe in the registry + # + # the manifest is a hash of { filename: url } + # where the url is the download url for the file + desc 'Package Digest' do + detail 'This feature was introduced in GitLab 12.5' + end + params do + requires :conan_package_reference, type: String, desc: 'Conan package ID' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get 'packages/:conan_package_reference/digest' do + present_package_download_urls + end + + desc 'Recipe Digest' do + detail 'This feature was introduced in GitLab 12.5' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get 'digest' do + present_recipe_download_urls + end + + # Get the download urls + # + # returns the download urls for the existing recipe or package in the registry + # + # the manifest is a hash of { filename: url } + # where the url is the download url for the file + desc 'Package Download Urls' do + detail 'This feature was introduced in GitLab 12.5' + end + + params do + requires :conan_package_reference, type: String, desc: 'Conan package ID' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get 'packages/:conan_package_reference/download_urls' do + present_package_download_urls + end + + desc 'Recipe Download Urls' do + detail 'This feature was introduced in GitLab 12.5' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get 'download_urls' do + present_recipe_download_urls + end + + # Get the upload urls + # + # request body contains { filename: filesize } where the filename is the + # name of the file the conan client is requesting to upload + # + # returns { filename: url } + # where the url is the upload url for the file that the conan client will use + desc 'Package Upload Urls' do + detail 'This feature was introduced in GitLab 12.4' + end + + params do + requires :conan_package_reference, type: String, desc: 'Conan package ID' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + post 'packages/:conan_package_reference/upload_urls' do + authorize!(:read_package, project) + + status 200 + present package_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls + end + + desc 'Recipe Upload Urls' do + detail 'This feature was introduced in GitLab 12.4' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + post 'upload_urls' do + authorize!(:read_package, project) + + status 200 + present recipe_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls + end + + desc 'Delete Package' do + detail 'This feature was introduced in GitLab 12.5' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + delete do + authorize!(:destroy_package, project) + + track_package_event('delete_package', :conan, category: 'API::ConanPackages') + + package.destroy + end + end + + params do + requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name' + requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version' + requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username' + requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel' + requires :recipe_revision, type: String, regexp: CONAN_REVISION_REGEX, desc: 'Conan Recipe Revision' + end + namespace 'files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision', requirements: PACKAGE_REQUIREMENTS do + before do + authenticate_non_get! + end + + params do + requires :file_name, type: String, desc: 'Package file name', values: CONAN_FILES + end + namespace 'export/:file_name', requirements: FILE_NAME_REQUIREMENTS do + desc 'Download recipe files' do + detail 'This feature was introduced in GitLab 12.6' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get do + download_package_file(:recipe_file) + end + + desc 'Upload recipe package files' do + detail 'This feature was introduced in GitLab 12.6' + end + + params do + requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + put do + upload_package_file(:recipe_file) + end + + desc 'Workhorse authorize the conan recipe file' do + detail 'This feature was introduced in GitLab 12.6' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + put 'authorize' do + authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size) + end + end + + params do + requires :conan_package_reference, type: String, desc: 'Conan Package ID' + requires :package_revision, type: String, desc: 'Conan Package Revision' + requires :file_name, type: String, desc: 'Package file name', values: CONAN_FILES + end + namespace 'package/:conan_package_reference/:package_revision/:file_name', requirements: FILE_NAME_REQUIREMENTS do + desc 'Download package files' do + detail 'This feature was introduced in GitLab 12.5' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + get do + download_package_file(:package_file) + end + + desc 'Workhorse authorize the conan package file' do + detail 'This feature was introduced in GitLab 12.6' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + put 'authorize' do + authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size) + end + + desc 'Upload package files' do + detail 'This feature was introduced in GitLab 12.6' + end + + params do + requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' + end + + route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true + + put do + upload_package_file(:package_file) + end + end + end + end + end + end + end +end diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb index a91db93b182..833288c6013 100644 --- a/lib/api/concerns/packages/npm_endpoints.rb +++ b/lib/api/concerns/packages/npm_endpoints.rb @@ -37,7 +37,7 @@ module API get 'dist-tags', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do package_name = params[:package_name] - bad_request!('Package Name') if package_name.blank? + bad_request_missing_attribute!('Package Name') if package_name.blank? authorize_read_package!(project) @@ -62,9 +62,9 @@ module API version = env['api.request.body'] tag = params[:tag] - bad_request!('Package Name') if package_name.blank? - bad_request!('Version') if version.blank? - bad_request!('Tag') if tag.blank? + bad_request_missing_attribute!('Package Name') if package_name.blank? + bad_request_missing_attribute!('Version') if version.blank? + bad_request_missing_attribute!('Tag') if tag.blank? authorize_create_package!(project) @@ -85,8 +85,8 @@ module API package_name = params[:package_name] tag = params[:tag] - bad_request!('Package Name') if package_name.blank? - bad_request!('Tag') if tag.blank? + bad_request_missing_attribute!('Package Name') if package_name.blank? + bad_request_missing_attribute!('Tag') if tag.blank? authorize_destroy_package!(project) diff --git a/lib/api/concerns/packages/nuget_endpoints.rb b/lib/api/concerns/packages/nuget_endpoints.rb new file mode 100644 index 00000000000..5177c4d23c0 --- /dev/null +++ b/lib/api/concerns/packages/nuget_endpoints.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true +# +# NuGet Package Manager Client API +# +# These API endpoints are not consumed directly by users, so there is no documentation for the +# individual endpoints. They are called by the NuGet package manager client when users run commands +# like `nuget install` or `nuget push`. The usage of the GitLab NuGet registry is documented here: +# https://docs.gitlab.com/ee/user/packages/nuget_repository/ +# +# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798 +module API + module Concerns + module Packages + module NugetEndpoints + extend ActiveSupport::Concern + + POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze + NON_NEGATIVE_INTEGER_REGEX = %r{\A0|[1-9]\d*\z}.freeze + + included do + helpers do + def find_packages + packages = package_finder.execute + + not_found!('Packages') unless packages.exists? + + packages + end + + def find_package + package = package_finder(package_version: params[:package_version]).execute + .first + + not_found!('Package') unless package + + package + end + + def package_finder(finder_params = {}) + ::Packages::Nuget::PackageFinder.new( + authorized_user_project, + **finder_params.merge(package_name: params[:package_name]) + ) + end + end + + # https://docs.microsoft.com/en-us/nuget/api/service-index + desc 'The NuGet Service Index' do + detail 'This feature was introduced in GitLab 12.6' + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + get 'index', format: :json do + authorize_read_package!(authorized_user_project) + track_package_event('cli_metadata', :nuget, category: 'API::NugetPackages') + + present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project), + with: ::API::Entities::Nuget::ServiceIndex + end + + # https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource + params do + requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX + end + namespace '/metadata/*package_name' do + before do + authorize_read_package!(authorized_user_project) + end + + desc 'The NuGet Metadata Service - Package name level' do + detail 'This feature was introduced in GitLab 12.8' + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + get 'index', format: :json do + present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages), + with: ::API::Entities::Nuget::PackagesMetadata + end + + desc 'The NuGet Metadata Service - Package name and version level' do + detail 'This feature was introduced in GitLab 12.8' + end + params do + requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + get '*package_version', format: :json do + present ::Packages::Nuget::PackageMetadataPresenter.new(find_package), + with: ::API::Entities::Nuget::PackageMetadata + end + end + + # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource + params do + requires :q, type: String, desc: 'The search term' + optional :skip, type: Integer, desc: 'The number of results to skip', default: 0, regexp: NON_NEGATIVE_INTEGER_REGEX + optional :take, type: Integer, desc: 'The number of results to return', default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX + optional :prerelease, type: ::Grape::API::Boolean, desc: 'Include prerelease versions', default: true + end + namespace '/query' do + before do + authorize_read_package!(authorized_user_project) + end + + desc 'The NuGet Search Service' do + detail 'This feature was introduced in GitLab 12.8' + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + get format: :json do + search_options = { + include_prerelease_versions: params[:prerelease], + per_page: params[:take], + padding: params[:skip] + } + search = ::Packages::Nuget::SearchService + .new(authorized_user_project, params[:q], search_options) + .execute + + track_package_event('search_package', :nuget, category: 'API::NugetPackages') + + present ::Packages::Nuget::SearchResultsPresenter.new(search), + with: ::API::Entities::Nuget::SearchResults + end + end + end + end + end + end +end diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 4c4ec200060..580d546b360 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -104,6 +104,7 @@ module API position: params[:position], id_key => noteable.id } + opts[:commit_id] = params[:commit_id] if noteable.is_a?(MergeRequest) && type == 'DiffNote' note = create_note(noteable, opts) diff --git a/lib/api/entities/cluster.rb b/lib/api/entities/cluster.rb index 67459092a33..b7e76e763f7 100644 --- a/lib/api/entities/cluster.rb +++ b/lib/api/entities/cluster.rb @@ -3,7 +3,7 @@ module API module Entities class Cluster < Grape::Entity - expose :id, :name, :created_at, :domain + expose :id, :name, :created_at, :domain, :enabled, :managed expose :provider_type, :platform_type, :environment_scope, :cluster_type, :namespace_per_environment expose :user, using: Entities::UserBasic expose :platform_kubernetes, using: Entities::Platform::Kubernetes diff --git a/lib/api/entities/feature.rb b/lib/api/entities/feature.rb index 618a7be9c7b..d1151849cd7 100644 --- a/lib/api/entities/feature.rb +++ b/lib/api/entities/feature.rb @@ -17,6 +17,16 @@ module API { key: gate.key, value: value } end.compact end + + class Definition < Grape::Entity + ::Feature::Definition::PARAMS.each do |param| + expose param + end + end + + expose :definition, using: Definition do |feature| + ::Feature::Definition.definitions[feature.name.to_sym] + end end end end diff --git a/lib/api/entities/feature_flag.rb b/lib/api/entities/feature_flag.rb index 82fdb20af00..f383eabd5dc 100644 --- a/lib/api/entities/feature_flag.rb +++ b/lib/api/entities/feature_flag.rb @@ -6,11 +6,11 @@ module API expose :name expose :description expose :active - expose :version, if: :feature_flags_new_version_enabled + expose :version expose :created_at expose :updated_at expose :scopes, using: FeatureFlag::LegacyScope - expose :strategies, using: FeatureFlag::Strategy, if: :feature_flags_new_version_enabled + expose :strategies, using: FeatureFlag::Strategy end end end diff --git a/lib/api/entities/issue.rb b/lib/api/entities/issue.rb index 5f2609cf68b..82102854394 100644 --- a/lib/api/entities/issue.rb +++ b/lib/api/entities/issue.rb @@ -43,6 +43,7 @@ module API end expose :moved_to_id + expose :service_desk_reply_to end end end diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb index 69523e3637b..7f1b5b87725 100644 --- a/lib/api/entities/merge_request_basic.rb +++ b/lib/api/entities/merge_request_basic.rb @@ -27,6 +27,7 @@ module API expose(:downvotes) { |merge_request, options| issuable_metadata.downvotes } expose :author, :assignees, :assignee, using: Entities::UserBasic + expose :reviewers, if: -> (merge_request, _) { merge_request.allows_reviewers? }, using: Entities::UserBasic expose :source_project_id, :target_project_id expose :labels do |merge_request, options| if options[:with_labels_details] diff --git a/lib/api/entities/note.rb b/lib/api/entities/note.rb index f22ab73afd0..9a60c04220d 100644 --- a/lib/api/entities/note.rb +++ b/lib/api/entities/note.rb @@ -14,6 +14,7 @@ module API expose :created_at, :updated_at expose :system?, as: :system expose :noteable_id, :noteable_type + expose :commit_id, if: ->(note, options) { note.noteable_type == "MergeRequest" && note.is_a?(DiffNote) } expose :position, if: ->(note, options) { note.is_a?(DiffNote) } do |note| note.position.to_h diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb index 82a44c75382..317caefe0a1 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -67,6 +67,8 @@ module API expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) } expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) } expose(:pages_access_level) { |project, options| project.project_feature.string_access_level(:pages) } + expose(:operations_access_level) { |project, options| project.project_feature.string_access_level(:operations) } + expose(:analytics_access_level) { |project, options| project.project_feature.string_access_level(:analytics) } expose :emails_disabled expose :shared_runners_enabled diff --git a/lib/api/entities/project_import_status.rb b/lib/api/entities/project_import_status.rb index f92593da3fa..e79c1cdf1a2 100644 --- a/lib/api/entities/project_import_status.rb +++ b/lib/api/entities/project_import_status.rb @@ -12,9 +12,8 @@ module API project.import_state&.relation_hard_failures(limit: 100) || [] end - # TODO: Use `expose_nil` once we upgrade the grape-entity gem - expose :import_error, if: lambda { |project, _ops| project.import_state&.last_error } do |project| - project.import_state.last_error + expose :import_error do |project, _options| + project.import_state&.last_error end end end diff --git a/lib/api/entities/project_snippet.rb b/lib/api/entities/project_snippet.rb index 8ed87e51375..253fcfcf38f 100644 --- a/lib/api/entities/project_snippet.rb +++ b/lib/api/entities/project_snippet.rb @@ -1,4 +1,4 @@ -# frozen_String_literal: true +# frozen_string_literal: true module API module Entities diff --git a/lib/api/entities/project_statistics.rb b/lib/api/entities/project_statistics.rb index 32201e88eaf..70980e670b0 100644 --- a/lib/api/entities/project_statistics.rb +++ b/lib/api/entities/project_statistics.rb @@ -10,6 +10,7 @@ module API expose :lfs_objects_size expose :build_artifacts_size, as: :job_artifacts_size expose :snippets_size + expose :packages_size end end end diff --git a/lib/api/entities/related_issue.rb b/lib/api/entities/related_issue.rb index 491c606bd49..60793fed5e0 100644 --- a/lib/api/entities/related_issue.rb +++ b/lib/api/entities/related_issue.rb @@ -5,6 +5,8 @@ module API class RelatedIssue < ::API::Entities::Issue expose :issue_link_id expose :issue_link_type, as: :link_type + expose :issue_link_created_at, as: :link_created_at + expose :issue_link_updated_at, as: :link_updated_at end end end diff --git a/lib/api/feature_flags.rb b/lib/api/feature_flags.rb index 67168ba9be6..6fdc4535be3 100644 --- a/lib/api/feature_flags.rb +++ b/lib/api/feature_flags.rb @@ -62,8 +62,6 @@ module API attrs = declared_params(include_missing: false) - ensure_post_version_2_flags_enabled! if attrs[:version] == 'new_version_flag' - rename_key(attrs, :scopes, :scopes_attributes) rename_key(attrs, :strategies, :strategies_attributes) update_value(attrs, :strategies_attributes) do |strategies| @@ -143,7 +141,7 @@ module API end desc 'Update a feature flag' do - detail 'This feature will be introduced in GitLab 13.1 if feature_flags_new_version feature flag is removed' + detail 'This feature was introduced in GitLab 13.2' success ::API::Entities::FeatureFlag end params do @@ -163,7 +161,6 @@ module API end end put do - not_found! unless feature_flags_new_version_enabled? authorize_update_feature_flag! render_api_error!('PUT operations are not supported for legacy feature flags', :unprocessable_entity) if feature_flag.legacy_flag? @@ -228,32 +225,17 @@ module API def present_entity(result) present result, - with: ::API::Entities::FeatureFlag, - feature_flags_new_version_enabled: feature_flags_new_version_enabled? - end - - def ensure_post_version_2_flags_enabled! - unless feature_flags_new_version_enabled? - render_api_error!('Version 2 flags are not enabled for this project', :unprocessable_entity) - end + with: ::API::Entities::FeatureFlag end def feature_flag - @feature_flag ||= if feature_flags_new_version_enabled? - user_project.operations_feature_flags.find_by_name!(params[:feature_flag_name]) - else - user_project.operations_feature_flags.legacy_flag.find_by_name!(params[:feature_flag_name]) - end + @feature_flag ||= user_project.operations_feature_flags.find_by_name!(params[:feature_flag_name]) end def new_version_flag_present? user_project.operations_feature_flags.new_version_flag.find_by_name(params[:name]).present? end - def feature_flags_new_version_enabled? - Feature.enabled?(:feature_flags_new_version, user_project, default_enabled: true) - end - def rename_key(hash, old_key, new_key) hash[new_key] = hash.delete(old_key) if hash.key?(old_key) hash diff --git a/lib/api/feature_flags_user_lists.rb b/lib/api/feature_flags_user_lists.rb index 086bcbcdc89..8577da173b1 100644 --- a/lib/api/feature_flags_user_lists.rb +++ b/lib/api/feature_flags_user_lists.rb @@ -54,7 +54,7 @@ module API end params do - requires :iid, type: String, desc: 'The internal id of the user list' + requires :iid, type: String, desc: 'The internal ID of the user list' end resource 'feature_flags_user_lists/:iid' do desc 'Get a single feature flag user list belonging to a project' do diff --git a/lib/api/features.rb b/lib/api/features.rb index 2c2e3e3d0c9..57bd7c38ad2 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -46,6 +46,15 @@ module API present features, with: Entities::Feature, current_user: current_user end + desc 'Get a list of all feature definitions' do + success Entities::Feature::Definition + end + get :definitions do + definitions = ::Feature::Definition.definitions.values.map(&:to_h) + + present definitions, with: Entities::Feature::Definition, current_user: current_user + end + desc 'Set the gate value for the given feature' do success Entities::Feature end @@ -56,6 +65,7 @@ module API optional :user, type: String, desc: 'A GitLab username' optional :group, type: String, desc: "A GitLab group's path, such as 'gitlab-org'" optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce' + optional :force, type: Boolean, desc: 'Skip feature flag validation checks, ie. YAML definition' mutually_exclusive :key, :feature_group mutually_exclusive :key, :user @@ -63,9 +73,8 @@ module API mutually_exclusive :key, :project end post ':name' do - validate_feature_flag_name!(params[:name]) + validate_feature_flag_name!(params[:name]) unless params[:force] - feature = Feature.get(params[:name]) # rubocop:disable Gitlab/AvoidFeatureGet targets = gate_targets(params) value = gate_value(params) key = gate_key(params) @@ -73,25 +82,26 @@ module API case value when true if gate_specified?(params) - targets.each { |target| feature.enable(target) } + targets.each { |target| Feature.enable(params[:name], target) } else - feature.enable + Feature.enable(params[:name]) end when false if gate_specified?(params) - targets.each { |target| feature.disable(target) } + targets.each { |target| Feature.disable(params[:name], target) } else - feature.disable + Feature.disable(params[:name]) end else if key == :percentage_of_actors - feature.enable_percentage_of_actors(value) + Feature.enable_percentage_of_actors(params[:name], value) else - feature.enable_percentage_of_time(value) + Feature.enable_percentage_of_time(params[:name], value) end end - present feature, with: Entities::Feature, current_user: current_user + present Feature.get(params[:name]), # rubocop:disable Gitlab/AvoidFeatureGet + with: Entities::Feature, current_user: current_user end desc 'Remove the gate value for the given feature' diff --git a/lib/api/go_proxy.rb b/lib/api/go_proxy.rb index 8fb4c561c40..2d978019f2a 100755 --- a/lib/api/go_proxy.rb +++ b/lib/api/go_proxy.rb @@ -48,7 +48,7 @@ module API not_found! unless Feature.enabled?(:go_proxy, user_project) module_name = case_decode params[:module_name] - bad_request!('Module Name') if module_name.blank? + bad_request_missing_attribute!('Module Name') if module_name.blank? mod = ::Packages::Go::ModuleFinder.new(user_project, module_name).execute diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb index ac5a1a2ce94..2bfd98a5b69 100644 --- a/lib/api/group_boards.rb +++ b/lib/api/group_boards.rb @@ -83,8 +83,6 @@ module API use :list_creation_params end post '/lists' do - authorize_list_type_resource! - authorize!(:admin_list, user_group) create_list diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb index a435b050042..81944a653c8 100644 --- a/lib/api/group_clusters.rb +++ b/lib/api/group_clusters.rb @@ -75,10 +75,12 @@ module API params do requires :cluster_id, type: Integer, desc: 'The cluster ID' optional :name, type: String, desc: 'Cluster name' + optional :enabled, type: Boolean, desc: 'Determines if cluster is active or not' optional :domain, type: String, desc: 'Cluster base domain' optional :environment_scope, type: String, desc: 'The associated environment to the cluster' optional :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace' optional :management_project_id, type: Integer, desc: 'The ID of the management project' + optional :managed, type: Boolean, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster' optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do optional :api_url, type: String, desc: 'URL to access the Kubernetes API' optional :token, type: String, desc: 'Token to authenticate against Kubernetes' diff --git a/lib/api/group_labels.rb b/lib/api/group_labels.rb index bf3ac8800b7..7fbf4445116 100644 --- a/lib/api/group_labels.rb +++ b/lib/api/group_labels.rb @@ -66,7 +66,7 @@ module API success Entities::GroupLabel end params do - optional :label_id, type: Integer, desc: 'The id of the label to be updated' + optional :label_id, type: Integer, desc: 'The ID of the label to be updated' optional :name, type: String, desc: 'The name of the label to be updated' use :group_label_update_params exactly_one_of :label_id, :name diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 147d8407142..6fe25471289 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -271,6 +271,10 @@ module API authorize! :read_build, user_project end + def authorize_read_build_trace!(build) + authorize! :read_build_trace, build + end + def authorize_destroy_artifacts! authorize! :destroy_artifacts, user_project end @@ -318,7 +322,7 @@ module API # keys (required) - A hash consisting of keys that must be present def required_attributes!(keys) keys.each do |key| - bad_request!(key) unless params[key].present? + bad_request_missing_attribute!(key) unless params[key].present? end end @@ -364,12 +368,16 @@ module API render_api_error!(message.join(' '), 403) end - def bad_request!(attribute) - message = ["400 (Bad request)"] - message << "\"" + attribute.to_s + "\" not given" if attribute + def bad_request!(reason = nil) + message = ['400 Bad request'] + message << "- #{reason}" if reason render_api_error!(message.join(' '), 400) end + def bad_request_missing_attribute!(attribute) + bad_request!("\"#{attribute}\" not given") + end + def not_found!(resource = nil) message = ["404"] message << resource if resource @@ -536,13 +544,23 @@ module API ) end + def increment_counter(event_name) + feature_name = "usage_data_#{event_name}" + return unless Feature.enabled?(feature_name) + + Gitlab::UsageDataCounters.count(event_name) + rescue => error + Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}") + end + # @param event_name [String] the event name # @param values [Array|String] the values counted def increment_unique_values(event_name, values) return unless values.present? - feature_name = "usage_data_#{event_name}" - return unless Feature.enabled?(feature_name) + feature_flag = "usage_data_#{event_name}" + + return unless Feature.enabled?(feature_flag, default_enabled: true) Gitlab::UsageDataCounters::HLLRedisCounter.track_event(values, event_name) rescue => error diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 69b53ea6c2f..12b0a053e79 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -31,8 +31,7 @@ module API def access_checker_for(actor, protocol) access_checker_klass.new(actor.key_or_user, container, protocol, authentication_abilities: ssh_authentication_abilities, - namespace_path: namespace_path, - repository_path: project_path, + repository_path: repository_path, redirected_path: redirected_path) end @@ -71,18 +70,22 @@ module API false end - def project_path - project&.path || project_path_match[:project_path] - end - - def namespace_path - project&.namespace&.full_path || project_path_match[:namespace_path] - end - private - def project_path_match - @project_path_match ||= params[:project].match(Gitlab::PathRegex.full_project_git_path_regex) || {} + def repository_path + if container + "#{container.full_path}.git" + elsif params[:project] + # When the project doesn't exist, we still need to pass on the path + # to support auto-creation in `GitAccessProject`. + # + # For consistency with the Git HTTP controllers, we normalize the path + # to remove a leading slash and ensure a trailing `.git`. + # + # NOTE: For GitLab Shell, `params[:project]` is the full repository path + # from the SSH command, with an optional trailing `.git`. + "#{params[:project].delete_prefix('/').delete_suffix('.git')}.git" + end end # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -96,7 +99,7 @@ module API end # rubocop:enable Gitlab/ModuleWithInstanceVariables - # Project id to pass between components that don't share/don't have + # Repository id to pass between components that don't share/don't have # access to the same filesystem mounts def gl_repository repo_type.identifier_for_container(container) @@ -106,8 +109,9 @@ module API repository.full_path end - # Return the repository depending on whether we want the wiki or the - # regular repository + # Return the repository for the detected type and container + # + # @returns [Repository] def repository @repository ||= repo_type.repository_for(container) end diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb index 431001c227d..8aed578905e 100644 --- a/lib/api/helpers/members_helpers.rb +++ b/lib/api/helpers/members_helpers.rb @@ -45,7 +45,7 @@ module API end def find_all_members_for_project(project) - MembersFinder.new(project, current_user).execute(include_relations: [:inherited, :direct, :invited_groups_members]) + MembersFinder.new(project, current_user).execute(include_relations: [:inherited, :direct, :invited_groups]) end def find_all_members_for_group(group) diff --git a/lib/api/helpers/packages/basic_auth_helpers.rb b/lib/api/helpers/packages/basic_auth_helpers.rb index e35a8712131..0784efc11d6 100644 --- a/lib/api/helpers/packages/basic_auth_helpers.rb +++ b/lib/api/helpers/packages/basic_auth_helpers.rb @@ -7,8 +7,8 @@ module API extend ::Gitlab::Utils::Override module Constants - AUTHENTICATE_REALM_HEADER = 'Www-Authenticate: Basic realm' - AUTHENTICATE_REALM_NAME = 'GitLab Packages Registry' + AUTHENTICATE_REALM_HEADER = 'WWW-Authenticate' + AUTHENTICATE_REALM_NAME = 'Basic realm="GitLab Packages Registry"' end include Constants diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb index 934e18bdd0a..39ecfc171a9 100644 --- a/lib/api/helpers/packages/conan/api_helpers.rb +++ b/lib/api/helpers/packages/conan/api_helpers.rb @@ -164,7 +164,11 @@ module API end def find_or_create_package - package || ::Packages::Conan::CreatePackageService.new(project, current_user, params).execute + package || ::Packages::Conan::CreatePackageService.new( + project, + current_user, + params.merge(build: current_authenticated_job) + ).execute end def track_push_package_event @@ -184,7 +188,11 @@ module API def create_package_file_with_type(file_type, current_package) unless params[:file].size == 0 # rubocop: disable Style/ZeroLengthPredicate # conan sends two upload requests, the first has no file, so we skip record creation if file.size == 0 - ::Packages::Conan::CreatePackageFileService.new(current_package, params[:file], params.merge(conan_file_type: file_type)).execute + ::Packages::Conan::CreatePackageFileService.new( + current_package, + params[:file], + params.merge(conan_file_type: file_type, build: current_authenticated_job) + ).execute end end @@ -214,6 +222,7 @@ module API return unless route_authentication_setting[:job_token_allowed] job = find_job_from_token || raise(::Gitlab::Auth::UnauthorizedError) + @current_authenticated_job = job # rubocop:disable Gitlab/ModuleWithInstanceVariables job.user end diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 0364ba2ad9e..f5f45cf7351 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -6,7 +6,7 @@ module API extend ActiveSupport::Concern extend Grape::API::Helpers - STATISTICS_SORT_PARAMS = %w[storage_size repository_size wiki_size].freeze + STATISTICS_SORT_PARAMS = %w[storage_size repository_size wiki_size packages_size].freeze params :optional_project_params_ce do optional :description, type: String, desc: 'The description of the project' @@ -32,6 +32,8 @@ module API optional :builds_access_level, type: String, values: %w(disabled private enabled), desc: 'Builds access level. One of `disabled`, `private` or `enabled`' optional :snippets_access_level, type: String, values: %w(disabled private enabled), desc: 'Snippets access level. One of `disabled`, `private` or `enabled`' optional :pages_access_level, type: String, values: %w(disabled private enabled public), desc: 'Pages access level. One of `disabled`, `private`, `enabled` or `public`' + optional :operations_access_level, type: String, values: %w(disabled private enabled), desc: 'Operations access level. One of `disabled`, `private` or `enabled`' + optional :analytics_access_level, type: String, values: %w(disabled private enabled), desc: 'Analytics access level. One of `disabled`, `private` or `enabled`' optional :emails_disabled, type: Boolean, desc: 'Disable email notifications' optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis' diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb index 4adb27a7414..9d2fd9978d9 100644 --- a/lib/api/helpers/services_helpers.rb +++ b/lib/api/helpers/services_helpers.rb @@ -304,6 +304,38 @@ module API desc: 'Project URL' } ], + 'datadog' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'API key used for authentication with Datadog' + }, + { + required: false, + name: :datadog_site, + type: String, + desc: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site' + }, + { + required: false, + name: :api_url, + type: String, + desc: '(Advanced) Define the full URL for your Datadog site directly' + }, + { + required: false, + name: :datadog_service, + type: String, + desc: 'Name of this GitLab instance that all data will be tagged with' + }, + { + required: false, + name: :datadog_env, + type: String, + desc: 'The environment tag that traces will be tagged with' + } + ], 'discord' => [ { required: true, @@ -459,6 +491,32 @@ module API desc: 'Colorize messages' } ], + 'jenkins' => [ + { + required: true, + name: :jenkins_url, + type: String, + desc: 'Jenkins root URL like https://jenkins.example.com' + }, + { + required: true, + name: :project_name, + type: String, + desc: 'The URL-friendly project name. Example: my_project_name' + }, + { + required: false, + name: :username, + type: String, + desc: 'A user with access to the Jenkins server, if applicable' + }, + { + required: false, + name: :password, + type: String, + desc: 'The password of the user' + } + ], 'jira' => [ { required: true, @@ -758,6 +816,7 @@ module API ::ConfluenceService, ::CampfireService, ::CustomIssueTrackerService, + ::DatadogService, ::DiscordService, ::DroneCiService, ::EmailsOnPushService, @@ -767,6 +826,7 @@ module API ::HangoutsChatService, ::HipchatService, ::IrkerService, + ::JenkinsService, ::JiraService, ::MattermostSlashCommandsService, ::SlackSlashCommandsService, @@ -787,7 +847,6 @@ module API def self.development_service_classes [ ::MockCiService, - ::MockDeploymentService, ::MockMonitoringService ] end diff --git a/lib/api/helpers/sse_helpers.rb b/lib/api/helpers/sse_helpers.rb new file mode 100644 index 00000000000..c354694f508 --- /dev/null +++ b/lib/api/helpers/sse_helpers.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Helpers + module SSEHelpers + def request_from_sse?(project) + return false if request.referer.blank? + + uri = URI.parse(request.referer) + uri.path.starts_with?(::Gitlab::Routing.url_helpers.project_root_sse_path(project)) + rescue URI::InvalidURIError + false + end + end + end +end diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index 61ef1d5bde0..332f2f1986f 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -300,7 +300,7 @@ module API post '/two_factor_otp_check', feature_category: :authentication_and_authorization do status 200 - break { success: false } unless Feature.enabled?(:two_factor_for_cli) + break { success: false, message: 'Feature flag is disabled' } unless Feature.enabled?(:two_factor_for_cli) actor.update_last_used_at! user = actor.user @@ -316,6 +316,8 @@ module API otp_validation_result = ::Users::ValidateOtpService.new(user).execute(params.fetch(:otp_attempt)) if otp_validation_result[:status] == :success + ::Gitlab::Auth::Otp::SessionEnforcer.new(actor.key).update_session + { success: true } else { success: false, message: 'Invalid OTP' } diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb index d4690709de4..73723a96401 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -85,9 +85,7 @@ module API get '/project_info' do project = find_project(params[:id]) - # TODO sort out authorization for real - # https://gitlab.com/gitlab-org/gitlab/-/issues/220912 - unless Ability.allowed?(nil, :download_code, project) + unless Guest.can?(:download_code, project) || agent.has_access_to?(project) not_found! end @@ -123,3 +121,5 @@ module API end end end + +API::Internal::Kubernetes.prepend_if_ee('EE::API::Internal::Kubernetes') diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb index 690f52d89f3..8eaeeae26c2 100644 --- a/lib/api/internal/pages.rb +++ b/lib/api/internal/pages.rb @@ -32,26 +32,29 @@ module API requires :host, type: String, desc: 'The host to query for' end get "/" do - serverless_domain_finder = ServerlessDomainFinder.new(params[:host]) - if serverless_domain_finder.serverless? - # Handle Serverless domains - serverless_domain = serverless_domain_finder.execute - no_content! unless serverless_domain - - virtual_domain = Serverless::VirtualDomain.new(serverless_domain) - no_content! unless virtual_domain - - present virtual_domain, with: Entities::Internal::Serverless::VirtualDomain - else - # Handle Pages domains - host = Namespace.find_by_pages_host(params[:host]) || PagesDomain.find_by_domain_case_insensitive(params[:host]) - no_content! unless host - - virtual_domain = host.pages_virtual_domain - no_content! unless virtual_domain - - present virtual_domain, with: Entities::Internal::Pages::VirtualDomain - end + ## + # Serverless domain proxy has been deprecated and disabled as per + # https://gitlab.com/gitlab-org/gitlab-pages/-/issues/467 + # + # serverless_domain_finder = ServerlessDomainFinder.new(params[:host]) + # if serverless_domain_finder.serverless? + # # Handle Serverless domains + # serverless_domain = serverless_domain_finder.execute + # no_content! unless serverless_domain + # + # virtual_domain = Serverless::VirtualDomain.new(serverless_domain) + # no_content! unless virtual_domain + # + # present virtual_domain, with: Entities::Internal::Serverless::VirtualDomain + # end + + host = Namespace.find_by_pages_host(params[:host]) || PagesDomain.find_by_domain_case_insensitive(params[:host]) + no_content! unless host + + virtual_domain = host.pages_virtual_domain + no_content! unless virtual_domain + + present virtual_domain, with: Entities::Internal::Pages::VirtualDomain end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 6a6ee7a4e1c..73e2163248d 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -435,3 +435,5 @@ module API end end end + +API::Issues.prepend_if_ee('EE::API::Issues') diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 51659c2e8a1..44751b3d76c 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -76,6 +76,8 @@ module API build = find_build!(params[:job_id]) + authorize_read_build_trace!(build) if build + header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" content_type 'text/plain' env['api.format'] = :binary diff --git a/lib/api/labels.rb b/lib/api/labels.rb index a8fc277989e..c9f29865664 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -57,7 +57,7 @@ module API success Entities::ProjectLabel end params do - optional :label_id, type: Integer, desc: 'The id of the label to be updated' + optional :label_id, type: Integer, desc: 'The ID of the label to be updated' optional :name, type: String, desc: 'The name of the label to be updated' use :project_label_update_params exactly_one_of :label_id, :name @@ -71,7 +71,7 @@ module API success Entities::ProjectLabel end params do - optional :label_id, type: Integer, desc: 'The id of the label to be deleted' + optional :label_id, type: Integer, desc: 'The ID of the label to be deleted' optional :name, type: String, desc: 'The name of the label to be deleted' exactly_one_of :label_id, :name end diff --git a/lib/api/members.rb b/lib/api/members.rb index 803de51651a..9bea74e2ce9 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -62,7 +62,7 @@ module API get ":id/members/:user_id" do source = find_source(source_type, params[:id]) - members = source.members + members = source_members(source) member = members.find_by!(user_id: params[:user_id]) present_members member diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb index 27ef0b9c7cd..00f42703731 100644 --- a/lib/api/merge_request_approvals.rb +++ b/lib/api/merge_request_approvals.rb @@ -4,7 +4,7 @@ module API class MergeRequestApprovals < ::API::Base before { authenticate_non_get! } - feature_category :code_review + feature_category :source_code_management helpers do params :ee_approval_params do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index d17e451093b..ab0e9b95e4a 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -11,6 +11,7 @@ module API feature_category :code_review helpers Helpers::MergeRequestsHelpers + helpers Helpers::SSEHelpers # EE::API::MergeRequests would override the following helpers helpers do @@ -216,6 +217,8 @@ module API handle_merge_request_errors!(merge_request) + Gitlab::UsageDataCounters::EditorUniqueCounter.track_sse_edit_action(author: current_user) if request_from_sse?(user_project) + present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project end diff --git a/lib/api/nuget_packages.rb b/lib/api/nuget_packages.rb deleted file mode 100644 index 65a85f3c930..00000000000 --- a/lib/api/nuget_packages.rb +++ /dev/null @@ -1,247 +0,0 @@ -# frozen_string_literal: true - -# NuGet Package Manager Client API -# -# These API endpoints are not meant to be consumed directly by users. They are -# called by the NuGet package manager client when users run commands -# like `nuget install` or `nuget push`. -module API - class NugetPackages < ::API::Base - helpers ::API::Helpers::PackagesManagerClientsHelpers - helpers ::API::Helpers::Packages::BasicAuthHelpers - - feature_category :package_registry - - POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze - NON_NEGATIVE_INTEGER_REGEX = %r{\A0|[1-9]\d*\z}.freeze - - PACKAGE_FILENAME = 'package.nupkg' - - default_format :json - - rescue_from ArgumentError do |e| - render_api_error!(e.message, 400) - end - - helpers do - def find_packages - packages = package_finder.execute - - not_found!('Packages') unless packages.exists? - - packages - end - - def find_package - package = package_finder(package_version: params[:package_version]).execute - .first - - not_found!('Package') unless package - - package - end - - def package_finder(finder_params = {}) - ::Packages::Nuget::PackageFinder.new( - authorized_user_project, - **finder_params.merge(package_name: params[:package_name]) - ) - end - end - - before do - require_packages_enabled! - end - - params do - requires :id, type: String, desc: 'The ID of a project', regexp: POSITIVE_INTEGER_REGEX - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - before do - authorized_user_project - end - - namespace ':id/packages/nuget' do - # https://docs.microsoft.com/en-us/nuget/api/service-index - desc 'The NuGet Service Index' do - detail 'This feature was introduced in GitLab 12.6' - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - get 'index', format: :json do - authorize_read_package!(authorized_user_project) - - track_package_event('cli_metadata', :nuget) - - present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project), - with: ::API::Entities::Nuget::ServiceIndex - end - - # https://docs.microsoft.com/en-us/nuget/api/package-publish-resource - desc 'The NuGet Package Publish endpoint' do - detail 'This feature was introduced in GitLab 12.6' - end - - params do - requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - put do - authorize_upload!(authorized_user_project) - bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size) - - file_params = params.merge( - file: params[:package], - file_name: PACKAGE_FILENAME - ) - - package = ::Packages::Nuget::CreatePackageService.new(authorized_user_project, current_user) - .execute - - package_file = ::Packages::CreatePackageFileService.new(package, file_params) - .execute - - track_package_event('push_package', :nuget) - - ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker - - created! - rescue ObjectStorage::RemoteStoreError => e - Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id }) - - forbidden! - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - put 'authorize' do - authorize_workhorse!( - subject: authorized_user_project, - has_length: false, - maximum_size: authorized_user_project.actual_limits.nuget_max_file_size - ) - end - - params do - requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX - end - namespace '/metadata/*package_name' do - before do - authorize_read_package!(authorized_user_project) - end - - # https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource - desc 'The NuGet Metadata Service - Package name level' do - detail 'This feature was introduced in GitLab 12.8' - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - get 'index', format: :json do - present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages), - with: ::API::Entities::Nuget::PackagesMetadata - end - - desc 'The NuGet Metadata Service - Package name and version level' do - detail 'This feature was introduced in GitLab 12.8' - end - params do - requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - get '*package_version', format: :json do - present ::Packages::Nuget::PackageMetadataPresenter.new(find_package), - with: ::API::Entities::Nuget::PackageMetadata - end - end - - # https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource - params do - requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX - end - namespace '/download/*package_name' do - before do - authorize_read_package!(authorized_user_project) - end - - desc 'The NuGet Content Service - index request' do - detail 'This feature was introduced in GitLab 12.8' - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - get 'index', format: :json do - present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages), - with: ::API::Entities::Nuget::PackagesVersions - end - - desc 'The NuGet Content Service - content request' do - detail 'This feature was introduced in GitLab 12.8' - end - params do - requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX - requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - get '*package_version/*package_filename', format: :nupkg do - filename = "#{params[:package_filename]}.#{params[:format]}" - package_file = ::Packages::PackageFileFinder.new(find_package, filename, with_file_name_like: true) - .execute - - not_found!('Package') unless package_file - - track_package_event('pull_package', :nuget) - - # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false - present_carrierwave_file!(package_file.file, supports_direct_download: false) - end - end - - params do - requires :q, type: String, desc: 'The search term' - optional :skip, type: Integer, desc: 'The number of results to skip', default: 0, regexp: NON_NEGATIVE_INTEGER_REGEX - optional :take, type: Integer, desc: 'The number of results to return', default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX - optional :prerelease, type: Boolean, desc: 'Include prerelease versions', default: true - end - namespace '/query' do - before do - authorize_read_package!(authorized_user_project) - end - - # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource - desc 'The NuGet Search Service' do - detail 'This feature was introduced in GitLab 12.8' - end - - route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true - - get format: :json do - search_options = { - include_prerelease_versions: params[:prerelease], - per_page: params[:take], - padding: params[:skip] - } - search = Packages::Nuget::SearchService - .new(authorized_user_project, params[:q], search_options) - .execute - - track_package_event('search_package', :nuget) - - present ::Packages::Nuget::SearchResultsPresenter.new(search), - with: ::API::Entities::Nuget::SearchResults - end - end - end - end - end -end diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb new file mode 100644 index 00000000000..b2516cc91f8 --- /dev/null +++ b/lib/api/nuget_project_packages.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +# NuGet Package Manager Client API +# +# These API endpoints are not meant to be consumed directly by users. They are +# called by the NuGet package manager client when users run commands +# like `nuget install` or `nuget push`. +module API + class NugetProjectPackages < ::API::Base + helpers ::API::Helpers::PackagesManagerClientsHelpers + helpers ::API::Helpers::Packages::BasicAuthHelpers + + feature_category :package_registry + + PACKAGE_FILENAME = 'package.nupkg' + + default_format :json + + rescue_from ArgumentError do |e| + render_api_error!(e.message, 400) + end + + before do + require_packages_enabled! + end + + params do + requires :id, type: String, desc: 'The ID of a project', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + before do + authorized_user_project + end + + namespace ':id/packages/nuget' do + include ::API::Concerns::Packages::NugetEndpoints + + # https://docs.microsoft.com/en-us/nuget/api/package-publish-resource + desc 'The NuGet Package Publish endpoint' do + detail 'This feature was introduced in GitLab 12.6' + end + + params do + requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + put do + authorize_upload!(authorized_user_project) + bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size) + + file_params = params.merge( + file: params[:package], + file_name: PACKAGE_FILENAME + ) + + package = ::Packages::Nuget::CreatePackageService.new( + authorized_user_project, + current_user, + declared_params.merge(build: current_authenticated_job) + ).execute + + package_file = ::Packages::CreatePackageFileService.new( + package, + file_params.merge(build: current_authenticated_job) + ).execute + + track_package_event('push_package', :nuget, category: 'API::NugetPackages') + + ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker + + created! + rescue ObjectStorage::RemoteStoreError => e + Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id }) + + forbidden! + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + put 'authorize' do + authorize_workhorse!( + subject: authorized_user_project, + has_length: false, + maximum_size: authorized_user_project.actual_limits.nuget_max_file_size + ) + end + + # https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource + params do + requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX + end + namespace '/download/*package_name' do + before do + authorize_read_package!(authorized_user_project) + end + + desc 'The NuGet Content Service - index request' do + detail 'This feature was introduced in GitLab 12.8' + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + get 'index', format: :json do + present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages), + with: ::API::Entities::Nuget::PackagesVersions + end + + desc 'The NuGet Content Service - content request' do + detail 'This feature was introduced in GitLab 12.8' + end + params do + requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX + requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX + end + + route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true + + get '*package_version/*package_filename', format: :nupkg do + filename = "#{params[:package_filename]}.#{params[:format]}" + package_file = ::Packages::PackageFileFinder.new(find_package, filename, with_file_name_like: true) + .execute + + not_found!('Package') unless package_file + + track_package_event('pull_package', :nuget, category: 'API::NugetPackages') + + # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false + present_carrierwave_file!(package_file.file, supports_direct_download: false) + end + end + end + end + end +end diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb index cfb0c5fd705..6785b28ddef 100644 --- a/lib/api/project_clusters.rb +++ b/lib/api/project_clusters.rb @@ -83,6 +83,8 @@ module API optional :environment_scope, type: String, desc: 'The associated environment to the cluster' optional :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace' optional :management_project_id, type: Integer, desc: 'The ID of the management project' + optional :enabled, type: Boolean, desc: 'Determines if cluster is active or not' + optional :managed, type: Boolean, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster' optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do optional :api_url, type: String, desc: 'URL to access the Kubernetes API' optional :token, type: String, desc: 'Token to authenticate against Kubernetes' diff --git a/lib/api/project_repository_storage_moves.rb b/lib/api/project_repository_storage_moves.rb index fe6de3ea385..196b7d88500 100644 --- a/lib/api/project_repository_storage_moves.rb +++ b/lib/api/project_repository_storage_moves.rb @@ -34,6 +34,22 @@ module API present storage_move, with: Entities::ProjectRepositoryStorageMove, current_user: current_user end + + desc 'Schedule bulk project repository storage moves' do + detail 'This feature was introduced in GitLab 13.7.' + end + params do + requires :source_storage_name, type: String, desc: 'The source storage shard', values: -> { Gitlab.config.repositories.storages.keys } + optional :destination_storage_name, type: String, desc: 'The destination storage shard', values: -> { Gitlab.config.repositories.storages.keys } + end + post do + ::Projects::ScheduleBulkRepositoryShardMovesService.enqueue( + declared_params[:source_storage_name], + declared_params[:destination_storage_name] + ) + + accepted! + end end params do diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index 7104fb8d999..658c6d13847 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -127,7 +127,7 @@ module API track_package_event('push_package', :pypi) ::Packages::Pypi::CreatePackageService - .new(authorized_user_project, current_user, declared_params) + .new(authorized_user_project, current_user, declared_params.merge(build: current_authenticated_job)) .execute created! diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index d3a185a51c8..52c73104bb4 100644 --- a/lib/api/release/links.rb +++ b/lib/api/release/links.rb @@ -57,7 +57,7 @@ module API end params do - requires :link_id, type: String, desc: 'The id of the link' + requires :link_id, type: String, desc: 'The ID of the link' end resource 'links/:link_id' do desc 'Get a link detail of a release' do diff --git a/lib/api/settings.rb b/lib/api/settings.rb index b95856d99d1..b3f09b431b0 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -52,6 +52,7 @@ module API optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility' optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility' + optional :disable_feed_token, type: Boolean, desc: 'Disable display of RSS/Atom and Calendar `feed_tokens`' optional :disabled_oauth_sign_in_sources, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Disable certain OAuth sign-in sources' optional :domain_denylist_enabled, type: Boolean, desc: 'Enable domain denylist for sign ups' optional :domain_denylist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' @@ -102,6 +103,11 @@ module API optional :performance_bar_allowed_group_id, type: String, desc: 'Deprecated: Use :performance_bar_allowed_group_path instead. Path of the group that is allowed to toggle the performance bar.' # support legacy names, can be removed in v6 optional :performance_bar_allowed_group_path, type: String, desc: 'Path of the group that is allowed to toggle the performance bar.' optional :performance_bar_enabled, type: String, desc: 'Deprecated: Pass `performance_bar_allowed_group_path: nil` instead. Allow enabling the performance.' # support legacy names, can be removed in v6 + optional :personal_access_token_prefix, type: String, desc: 'Prefix to prepend to all personal access tokens' + optional :kroki_enabled, type: Boolean, desc: 'Enable Kroki' + given kroki_enabled: ->(val) { val } do + requires :kroki_url, type: String, desc: 'The Kroki server URL' + end optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML' given plantuml_enabled: ->(val) { val } do requires :plantuml_url, type: String, desc: 'The PlantUML server URL' diff --git a/lib/api/statistics.rb b/lib/api/statistics.rb index 1814e1a6782..6818c04fd2e 100644 --- a/lib/api/statistics.rb +++ b/lib/api/statistics.rb @@ -4,7 +4,7 @@ module API class Statistics < ::API::Base before { authenticated_as_admin! } - feature_category :instance_statistics + feature_category :devops_reports COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue, MergeRequest, Note, Snippet, Key, Milestone].freeze diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb index 7b038ec74bb..cad2f52e951 100644 --- a/lib/api/usage_data.rb +++ b/lib/api/usage_data.rb @@ -20,6 +20,18 @@ module API requires :event, type: String, desc: 'The event name that should be tracked' end + post 'increment_counter' do + event_name = params[:event] + + increment_counter(event_name) + + status :ok + end + + params do + requires :event, type: String, desc: 'The event name that should be tracked' + end + post 'increment_unique_users' do event_name = params[:event] diff --git a/lib/api/users.rb b/lib/api/users.rb index 501ed629c7e..8b9b82877f7 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -534,6 +534,24 @@ module API user.activate end + + desc 'Approve a pending user. Available only for admins.' + params do + requires :id, type: Integer, desc: 'The ID of the user' + end + post ':id/approve', feature_category: :authentication_and_authorization do + user = User.find_by(id: params[:id]) + not_found!('User') unless can?(current_user, :read_user, user) + + result = ::Users::ApproveService.new(current_user).execute(user) + + if result[:success] + result + else + render_api_error!(result[:message], result[:http_status]) + end + end + # rubocop: enable CodeReuse/ActiveRecord desc 'Deactivate an active user. Available only for admins.' params do diff --git a/lib/api/validations/validators/absence.rb b/lib/api/validations/validators/absence.rb index 1f43f3ab126..7858ce7140b 100644 --- a/lib/api/validations/validators/absence.rb +++ b/lib/api/validations/validators/absence.rb @@ -7,7 +7,7 @@ module API def validate_param!(attr_name, params) return if params.respond_to?(:key?) && !params.key?(attr_name) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence) + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:absence)) end end end diff --git a/lib/api/validations/validators/array_none_any.rb b/lib/api/validations/validators/array_none_any.rb index 7efb8e6ccee..3732c1f575c 100644 --- a/lib/api/validations/validators/array_none_any.rb +++ b/lib/api/validations/validators/array_none_any.rb @@ -10,8 +10,10 @@ module API return if value.is_a?(Array) || [IssuableFinder::Params::FILTER_NONE, IssuableFinder::Params::FILTER_ANY].include?(value.to_s.downcase) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], - message: "should be an array, 'None' or 'Any'" + raise Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: "should be an array, 'None' or 'Any'" + ) end end end diff --git a/lib/api/validations/validators/check_assignees_count.rb b/lib/api/validations/validators/check_assignees_count.rb index b614058e325..92ada159b46 100644 --- a/lib/api/validations/validators/check_assignees_count.rb +++ b/lib/api/validations/validators/check_assignees_count.rb @@ -18,9 +18,10 @@ module API def validate_param!(attr_name, params) return if param_allowed?(attr_name, params) - raise Grape::Exceptions::Validation, - params: [@scope.full_name(attr_name)], - message: "allows one value, but found #{params[attr_name].size}: #{params[attr_name].join(", ")}" + raise Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: "allows one value, but found #{params[attr_name].size}: #{params[attr_name].join(", ")}" + ) end private diff --git a/lib/api/validations/validators/email_or_email_list.rb b/lib/api/validations/validators/email_or_email_list.rb index b7f2a0cd443..da665f39130 100644 --- a/lib/api/validations/validators/email_or_email_list.rb +++ b/lib/api/validations/validators/email_or_email_list.rb @@ -11,9 +11,10 @@ module API return if value.split(',').map { |v| ValidateEmail.valid?(v) }.all? - raise Grape::Exceptions::Validation, + raise Grape::Exceptions::Validation.new( params: [@scope.full_name(attr_name)], message: "contains an invalid email address" + ) end end end diff --git a/lib/api/validations/validators/file_path.rb b/lib/api/validations/validators/file_path.rb index 8a815c3b2b8..a6a3c692fd6 100644 --- a/lib/api/validations/validators/file_path.rb +++ b/lib/api/validations/validators/file_path.rb @@ -11,8 +11,10 @@ module API path = Gitlab::Utils.check_path_traversal!(path) Gitlab::Utils.check_allowed_absolute_path!(path, path_allowlist) rescue - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], - message: "should be a valid file path" + raise Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: "should be a valid file path" + ) end end end diff --git a/lib/api/validations/validators/git_ref.rb b/lib/api/validations/validators/git_ref.rb index 1dda9d758a7..dcb1db6ca33 100644 --- a/lib/api/validations/validators/git_ref.rb +++ b/lib/api/validations/validators/git_ref.rb @@ -17,8 +17,10 @@ module API return unless invalid_character?(revision) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], - message: 'should be a valid reference path' + raise Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: 'should be a valid reference path' + ) end private diff --git a/lib/api/validations/validators/git_sha.rb b/lib/api/validations/validators/git_sha.rb index 657307db1df..665d1878b4c 100644 --- a/lib/api/validations/validators/git_sha.rb +++ b/lib/api/validations/validators/git_sha.rb @@ -9,8 +9,10 @@ module API return if Commit::EXACT_COMMIT_SHA_PATTERN.match?(sha) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], - message: "should be a valid sha" + raise Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: "should be a valid sha" + ) end end end diff --git a/lib/api/validations/validators/integer_none_any.rb b/lib/api/validations/validators/integer_none_any.rb index aa8c137a6ab..32ab6e19b98 100644 --- a/lib/api/validations/validators/integer_none_any.rb +++ b/lib/api/validations/validators/integer_none_any.rb @@ -3,15 +3,11 @@ module API module Validations module Validators - class IntegerNoneAny < Grape::Validations::Base - def validate_param!(attr_name, params) - value = params[attr_name] + class IntegerNoneAny < IntegerOrCustomValue + private - return if value.is_a?(Integer) || - [IssuableFinder::Params::FILTER_NONE, IssuableFinder::Params::FILTER_ANY].include?(value.to_s.downcase) - - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], - message: "should be an integer, 'None' or 'Any'" + def extract_custom_values(_options) + [IssuableFinder::Params::FILTER_NONE, IssuableFinder::Params::FILTER_ANY] end end end diff --git a/lib/api/validations/validators/integer_or_custom_value.rb b/lib/api/validations/validators/integer_or_custom_value.rb new file mode 100644 index 00000000000..d2352495948 --- /dev/null +++ b/lib/api/validations/validators/integer_or_custom_value.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module API + module Validations + module Validators + class IntegerOrCustomValue < Grape::Validations::Base + def initialize(attrs, options, required, scope, **opts) + @custom_values = extract_custom_values(options) + super + end + + def validate_param!(attr_name, params) + value = params[attr_name] + + return if value.is_a?(Integer) + return if @custom_values.map(&:downcase).include?(value.to_s.downcase) + + valid_options = Gitlab::Utils.to_exclusive_sentence(['an integer'] + @custom_values) + raise Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: "should be #{valid_options}, however got #{value}" + ) + end + + private + + def extract_custom_values(options) + options.is_a?(Hash) ? options[:values] : options + end + end + end + end +end diff --git a/lib/api/validations/validators/limit.rb b/lib/api/validations/validators/limit.rb index 3bb4cee1d75..e8f894849a5 100644 --- a/lib/api/validations/validators/limit.rb +++ b/lib/api/validations/validators/limit.rb @@ -9,9 +9,10 @@ module API return if value.size <= @option - raise Grape::Exceptions::Validation, + raise Grape::Exceptions::Validation.new( params: [@scope.full_name(attr_name)], message: "#{@scope.full_name(attr_name)} must be less than #{@option} characters" + ) end end end diff --git a/lib/api/validations/validators/untrusted_regexp.rb b/lib/api/validations/validators/untrusted_regexp.rb index ec623684e67..3ddea2bd9de 100644 --- a/lib/api/validations/validators/untrusted_regexp.rb +++ b/lib/api/validations/validators/untrusted_regexp.rb @@ -11,7 +11,7 @@ module API Gitlab::UntrustedRegexp.new(value) rescue RegexpError => e message = "is an invalid regexp: #{e.message}" - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message) end end end diff --git a/lib/atlassian/jira_connect/client.rb b/lib/atlassian/jira_connect/client.rb index f81ed462174..da24d0e20ee 100644 --- a/lib/atlassian/jira_connect/client.rb +++ b/lib/atlassian/jira_connect/client.rb @@ -12,31 +12,68 @@ module Atlassian @shared_secret = shared_secret end + def send_info(project:, update_sequence_id: nil, **args) + common = { project: project, update_sequence_id: update_sequence_id } + dev_info = args.slice(:commits, :branches, :merge_requests) + build_info = args.slice(:pipelines) + + responses = [] + + responses << store_dev_info(**common, **dev_info) if dev_info.present? + responses << store_build_info(**common, **build_info) if build_info.present? + raise ArgumentError, 'Invalid arguments' if responses.empty? + + responses.compact + end + + private + + def store_build_info(project:, pipelines:, update_sequence_id: nil) + return unless Feature.enabled?(:jira_sync_builds, project) + + builds = pipelines.map do |pipeline| + build = Serializers::BuildEntity.represent( + pipeline, + update_sequence_id: update_sequence_id + ) + next if build.issue_keys.empty? + + build + end.compact + return if builds.empty? + + post('/rest/builds/0.1/bulk', { builds: builds }) + end + def store_dev_info(project:, commits: nil, branches: nil, merge_requests: nil, update_sequence_id: nil) - dev_info_json = { - repositories: [ - Serializers::RepositoryEntity.represent( - project, - commits: commits, - branches: branches, - merge_requests: merge_requests, - user_notes_count: user_notes_count(merge_requests), - update_sequence_id: update_sequence_id - ) - ] - }.to_json - - uri = URI.join(@base_uri, '/rest/devinfo/0.10/bulk') - - headers = { + repo = Serializers::RepositoryEntity.represent( + project, + commits: commits, + branches: branches, + merge_requests: merge_requests, + user_notes_count: user_notes_count(merge_requests), + update_sequence_id: update_sequence_id + ) + + post('/rest/devinfo/0.10/bulk', { repositories: [repo] }) + end + + def post(path, payload) + uri = URI.join(@base_uri, path) + + self.class.post(uri, headers: headers(uri), body: metadata.merge(payload).to_json) + end + + def headers(uri) + { 'Authorization' => "JWT #{jwt_token('POST', uri)}", 'Content-Type' => 'application/json' } - - self.class.post(uri, headers: headers, body: dev_info_json) end - private + def metadata + { providerMetadata: { product: "GitLab #{Gitlab::VERSION}" } } + end def user_notes_count(merge_requests) return unless merge_requests diff --git a/lib/atlassian/jira_connect/serializers/base_entity.rb b/lib/atlassian/jira_connect/serializers/base_entity.rb index 94deb174a45..640337c0399 100644 --- a/lib/atlassian/jira_connect/serializers/base_entity.rb +++ b/lib/atlassian/jira_connect/serializers/base_entity.rb @@ -11,6 +11,12 @@ module Atlassian expose :update_sequence_id, as: :updateSequenceId + def eql(other) + other.is_a?(self.class) && to_json == other.to_json + end + + alias_method :==, :eql + private def update_sequence_id diff --git a/lib/atlassian/jira_connect/serializers/build_entity.rb b/lib/atlassian/jira_connect/serializers/build_entity.rb new file mode 100644 index 00000000000..3eb8b1f1978 --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/build_entity.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + # A Jira 'build' represents what we call a 'pipeline' + class BuildEntity < Grape::Entity + include Gitlab::Routing + + format_with(:iso8601, &:iso8601) + + expose :schema_version, as: :schemaVersion + expose :pipeline_id, as: :pipelineId + expose :iid, as: :buildNumber + expose :update_sequence_id, as: :updateSequenceNumber + expose :source_ref, as: :displayName + expose :url + expose :state + expose :updated_at, as: :lastUpdated, format_with: :iso8601 + expose :issue_keys, as: :issueKeys + expose :test_info, as: :testInfo + expose :references + + def issue_keys + # extract Jira issue keys from either the source branch/ref or the + # merge request title. + @issue_keys ||= begin + src = "#{pipeline.source_ref} #{pipeline.merge_request&.title}" + JiraIssueKeyExtractor.new(src).issue_keys + end + end + + private + + alias_method :pipeline, :object + delegate :project, to: :object + + def url + project_pipeline_url(project, pipeline) + end + + # translate to Jira status + def state + case pipeline.status + when 'scheduled', 'created', 'pending', 'preparing', 'waiting_for_resource' then 'pending' + when 'running' then 'in_progress' + when 'success' then 'successful' + when 'failed' then 'failed' + when 'canceled', 'skipped' then 'cancelled' + else + 'unknown' + end + end + + def pipeline_id + pipeline.ensure_ci_ref! + + pipeline.ci_ref.id.to_s + end + + def schema_version + '1.0' + end + + def test_info + builds = pipeline.builds.pluck(:status) # rubocop: disable CodeReuse/ActiveRecord + n = builds.size + passed = builds.count { |s| s == 'success' } + failed = builds.count { |s| s == 'failed' } + + { + totalNumber: n, + numberPassed: passed, + numberFailed: failed, + numberSkipped: n - (passed + failed) + } + end + + def references + ref = pipeline.source_ref + + [{ + commit: { id: pipeline.sha, repositoryUri: project_url(project) }, + ref: { name: ref, uri: project_commits_url(project, ref) } + }] + end + + def update_sequence_id + options[:update_sequence_id] || Client.generate_update_sequence_id + end + end + end + end +end diff --git a/lib/backup/files.rb b/lib/backup/files.rb index a0948f8c0f5..0f6ed847dea 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -26,9 +26,15 @@ module Backup FileUtils.rm_f(backup_tarball) if ENV['STRATEGY'] == 'copy' - cmd = [%w[rsync -a], exclude_dirs(:rsync), %W[#{app_files_dir} #{Gitlab.config.backup.path}]].flatten + cmd = [%w[rsync -a --delete], exclude_dirs(:rsync), %W[#{app_files_dir} #{Gitlab.config.backup.path}]].flatten output, status = Gitlab::Popen.popen(cmd) + # Retry if rsync source files vanish + if status == 24 + $stdout.puts "Warning: files vanished during rsync, retrying..." + output, status = Gitlab::Popen.popen(cmd) + end + unless status == 0 puts output raise Backup::Error, 'Backup failed' diff --git a/lib/banzai/filter/ascii_doc_sanitization_filter.rb b/lib/banzai/filter/ascii_doc_sanitization_filter.rb index a1a204ec652..11762c3bfb4 100644 --- a/lib/banzai/filter/ascii_doc_sanitization_filter.rb +++ b/lib/banzai/filter/ascii_doc_sanitization_filter.rb @@ -6,8 +6,8 @@ module Banzai # # Extends Banzai::Filter::BaseSanitizationFilter with specific rules. class AsciiDocSanitizationFilter < Banzai::Filter::BaseSanitizationFilter - # Section anchor link pattern - SECTION_LINK_REF_PATTERN = /\A#{Gitlab::Asciidoc::DEFAULT_ADOC_ATTRS['idprefix']}(:?[[:alnum:]]|-|_)+\z/.freeze + # Anchor link prefixed by "user-content-" pattern + PREFIXED_ID_PATTERN = /\A#{Gitlab::Asciidoc::DEFAULT_ADOC_ATTRS['idprefix']}(:?[[:alnum:]]|-|_)+\z/.freeze SECTION_HEADINGS = %w(h2 h3 h4 h5 h6).freeze # Footnote link patterns @@ -54,43 +54,34 @@ module Banzai whitelist[:attributes]['table'] = %w(class) whitelist[:transformers].push(self.class.remove_element_classes) + # Allow `id` in anchor and footnote elements + whitelist[:attributes]['a'].push('id') + whitelist[:attributes]['div'].push('id') + # Allow `id` in heading elements for section anchors SECTION_HEADINGS.each do |header| whitelist[:attributes][header] = %w(id) end - whitelist[:transformers].push(self.class.remove_non_heading_ids) - # Allow `id` in footnote elements - FOOTNOTE_LINK_ID_PATTERNS.keys.each do |element| - whitelist[:attributes][element.to_s].push('id') - end - whitelist[:transformers].push(self.class.remove_non_footnote_ids) + # Remove ids that are not explicitly allowed + whitelist[:transformers].push(self.class.remove_disallowed_ids) whitelist end class << self - def remove_non_footnote_ids + def remove_disallowed_ids lambda do |env| node = env[:node] - return unless (pattern = FOOTNOTE_LINK_ID_PATTERNS[node.name.to_sym]) + return unless node.name == 'a' || node.name == 'div' || SECTION_HEADINGS.any?(node.name) return unless node.has_attribute?('id') - return if node['id'] =~ pattern - - node.remove_attribute('id') - end - end - - def remove_non_heading_ids - lambda do |env| - node = env[:node] - - return unless SECTION_HEADINGS.any?(node.name) - return unless node.has_attribute?('id') + return if node['id'] =~ PREFIXED_ID_PATTERN - return if node['id'] =~ SECTION_LINK_REF_PATTERN + if (pattern = FOOTNOTE_LINK_ID_PATTERNS[node.name.to_sym]) + return if node['id'] =~ pattern + end node.remove_attribute('id') end diff --git a/lib/banzai/filter/base_sanitization_filter.rb b/lib/banzai/filter/base_sanitization_filter.rb index fc3791e0cbf..4f9e8cffd11 100644 --- a/lib/banzai/filter/base_sanitization_filter.rb +++ b/lib/banzai/filter/base_sanitization_filter.rb @@ -25,7 +25,7 @@ module Banzai # Allow data-math-style attribute in order to support LaTeX formatting whitelist[:attributes]['code'] = %w(data-math-style) - whitelist[:attributes]['pre'] = %w(data-math-style data-mermaid-style) + whitelist[:attributes]['pre'] = %w(data-math-style data-mermaid-style data-kroki-style) # Allow html5 details/summary elements whitelist[:elements].push('details') diff --git a/lib/banzai/filter/kroki_filter.rb b/lib/banzai/filter/kroki_filter.rb new file mode 100644 index 00000000000..dbd4de32a47 --- /dev/null +++ b/lib/banzai/filter/kroki_filter.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "nokogiri" +require "asciidoctor/extensions/asciidoctor_kroki/extension" + +module Banzai + module Filter + # HTML that replaces all diagrams supported by Kroki with the corresponding img tags. + # + class KrokiFilter < HTML::Pipeline::Filter + def call + return doc unless settings.kroki_enabled + + diagram_selectors = ::Gitlab::Kroki.formats(settings) + .map { |diagram_type| %(pre[lang="#{diagram_type}"] > code) } + .join(', ') + + return doc unless doc.at(diagram_selectors) + + diagram_format = "svg" + doc.css(diagram_selectors).each do |node| + diagram_type = node.parent['lang'] + img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{create_image_src(diagram_type, diagram_format, node.content)}"/>)) + node.parent.replace(img_tag) + end + + doc + end + + private + + def create_image_src(type, format, text) + ::AsciidoctorExtensions::KrokiDiagram.new(type, format, text) + .get_diagram_uri(settings.kroki_url) + end + + def settings + Gitlab::CurrentSettings.current_application_settings + end + end + end +end diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb index f05902078dc..0b8bd17a71b 100644 --- a/lib/banzai/filter/merge_request_reference_filter.rb +++ b/lib/banzai/filter/merge_request_reference_filter.rb @@ -59,7 +59,7 @@ module Banzai super(object_sym, tooltip: false) end - def data_attributes_for(text, parent, object, data = {}) + def data_attributes_for(text, parent, object, **data) super.merge(project_path: parent.full_path, iid: object.iid, mr_title: object.title) end diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index 6dc0cce6050..1d3bbe43344 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'rouge/plugins/common_mark' +require "asciidoctor/extensions/asciidoctor_kroki/extension" # Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/code_block.js module Banzai @@ -14,7 +15,7 @@ module Banzai LANG_PARAMS_ATTR = 'data-lang-params' def call - doc.search('pre:not([data-math-style]):not([data-mermaid-style]) > code').each do |node| + doc.search('pre:not([data-math-style]):not([data-mermaid-style]):not([data-kroki-style]) > code').each do |node| highlight_node(node) end @@ -86,7 +87,7 @@ module Banzai end def use_rouge?(language) - %w(math mermaid plantuml suggestion).exclude?(language) + (%w(math suggestion) + ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES).exclude?(language) end end end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 7057ac9d707..344afc9b33c 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -19,6 +19,7 @@ module Banzai Filter::SyntaxHighlightFilter, Filter::MathFilter, Filter::ColorFilter, + Filter::KrokiFilter, Filter::MermaidFilter, Filter::VideoLinkFilter, Filter::AudioLinkFilter, diff --git a/lib/bulk_imports/common/extractors/graphql_extractor.rb b/lib/bulk_imports/common/extractors/graphql_extractor.rb index 7d58032cfcc..c0cef61d2b2 100644 --- a/lib/bulk_imports/common/extractors/graphql_extractor.rb +++ b/lib/bulk_imports/common/extractors/graphql_extractor.rb @@ -6,15 +6,16 @@ module BulkImports class GraphqlExtractor def initialize(query) @query = query[:query] - @query_string = @query.to_s - @variables = @query.variables end def extract(context) - @context = context + client = graphql_client(context) Enumerator.new do |yielder| - result = graphql_client.execute(parsed_query, query_variables(context.entity)) + result = client.execute( + client.parse(query.to_s), + query.variables(context.entity) + ) yielder << result.original_hash.deep_dup end @@ -22,23 +23,17 @@ module BulkImports private - def graphql_client + attr_reader :query + + def graphql_client(context) @graphql_client ||= BulkImports::Clients::Graphql.new( - url: @context.configuration.url, - token: @context.configuration.access_token + url: context.configuration.url, + token: context.configuration.access_token ) end def parsed_query - @parsed_query ||= graphql_client.parse(@query.to_s) - end - - def query_variables(entity) - return unless @variables - - @variables.transform_values do |entity_attribute| - entity.public_send(entity_attribute) # rubocop:disable GitlabSecurity/PublicSend - end + @parsed_query ||= graphql_client.parse(query.to_s) end end end diff --git a/lib/bulk_imports/common/transformers/graphql_cleaner_transformer.rb b/lib/bulk_imports/common/transformers/graphql_cleaner_transformer.rb deleted file mode 100644 index dce0fac6999..00000000000 --- a/lib/bulk_imports/common/transformers/graphql_cleaner_transformer.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -# Cleanup GraphQL original response hash from unnecessary nesting -# 1. Remove ['data']['group'] or ['data']['project'] hash nesting -# 2. Remove ['edges'] & ['nodes'] array wrappings -# 3. Remove ['node'] hash wrapping -# -# @example -# data = {"data"=>{"group"=> { -# "name"=>"test", -# "fullName"=>"test", -# "description"=>"test", -# "labels"=>{"edges"=>[{"node"=>{"title"=>"label1"}}, {"node"=>{"title"=>"label2"}}, {"node"=>{"title"=>"label3"}}]}}}} -# -# BulkImports::Common::Transformers::GraphqlCleanerTransformer.new.transform(nil, data) -# -# {"name"=>"test", "fullName"=>"test", "description"=>"test", "labels"=>[{"title"=>"label1"}, {"title"=>"label2"}, {"title"=>"label3"}]} -module BulkImports - module Common - module Transformers - class GraphqlCleanerTransformer - EDGES = 'edges' - NODE = 'node' - - def initialize(options = {}) - @options = options - end - - def transform(_, data) - return data unless data.is_a?(Hash) - - data = data.dig('data', 'group') || data.dig('data', 'project') || data - - clean_edges_and_nodes(data) - end - - def clean_edges_and_nodes(data) - case data - when Array - data.map(&method(:clean_edges_and_nodes)) - when Hash - if data.key?(NODE) - clean_edges_and_nodes(data[NODE]) - else - data.transform_values { |value| clean_edges_and_nodes(value.try(:fetch, EDGES, value) || value) } - end - else - data - end - end - end - end - end -end diff --git a/lib/bulk_imports/common/transformers/hash_key_digger.rb b/lib/bulk_imports/common/transformers/hash_key_digger.rb new file mode 100644 index 00000000000..b4897b5b2bf --- /dev/null +++ b/lib/bulk_imports/common/transformers/hash_key_digger.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module BulkImports + module Common + module Transformers + class HashKeyDigger + def initialize(options = {}) + @key_path = options[:key_path] + end + + def transform(_, data) + raise ArgumentError, "Given data must be a Hash" unless data.is_a?(Hash) + + data.dig(*Array.wrap(key_path)) + end + + private + + attr_reader :key_path + end + end + end +end diff --git a/lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb b/lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb new file mode 100644 index 00000000000..858c4c8976b --- /dev/null +++ b/lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module BulkImports + module Common + module Transformers + class ProhibitedAttributesTransformer + PROHIBITED_REFERENCES = Regexp.union( + /\Acached_markdown_version\Z/, + /\Aid\Z/, + /_id\Z/, + /_ids\Z/, + /_html\Z/, + /attributes/, + /\Aremote_\w+_(url|urls|request_header)\Z/ # carrierwave automatically creates these attribute methods for uploads + ).freeze + + def initialize(options = {}) + @options = options + end + + def transform(context, data) + data.each_with_object({}) do |(key, value), result| + prohibited = prohibited_key?(key) + + unless prohibited + result[key] = value.is_a?(Hash) ? transform(context, value) : value + end + end + end + + private + + def prohibited_key?(key) + key.to_s =~ PROHIBITED_REFERENCES + end + end + end + end +end diff --git a/lib/bulk_imports/groups/graphql/get_group_query.rb b/lib/bulk_imports/groups/graphql/get_group_query.rb index c50b99aae4e..2bc0f60baa2 100644 --- a/lib/bulk_imports/groups/graphql/get_group_query.rb +++ b/lib/bulk_imports/groups/graphql/get_group_query.rb @@ -29,8 +29,8 @@ module BulkImports GRAPHQL end - def variables - { full_path: :source_full_path } + def variables(entity) + { full_path: entity.source_full_path } end end end diff --git a/lib/bulk_imports/groups/pipelines/group_pipeline.rb b/lib/bulk_imports/groups/pipelines/group_pipeline.rb index 2b7d0ef7658..5169e292180 100644 --- a/lib/bulk_imports/groups/pipelines/group_pipeline.rb +++ b/lib/bulk_imports/groups/pipelines/group_pipeline.rb @@ -6,10 +6,13 @@ module BulkImports class GroupPipeline include Pipeline + abort_on_failure! + extractor Common::Extractors::GraphqlExtractor, query: Graphql::GetGroupQuery - transformer Common::Transformers::GraphqlCleanerTransformer + transformer Common::Transformers::HashKeyDigger, key_path: %w[data group] transformer Common::Transformers::UnderscorifyKeysTransformer + transformer Common::Transformers::ProhibitedAttributesTransformer transformer Groups::Transformers::GroupAttributesTransformer loader Groups::Loaders::GroupLoader diff --git a/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline.rb b/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline.rb index 6384e9d5972..d7e1a118d0b 100644 --- a/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline.rb +++ b/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline.rb @@ -7,6 +7,7 @@ module BulkImports include Pipeline extractor BulkImports::Groups::Extractors::SubgroupsExtractor + transformer Common::Transformers::ProhibitedAttributesTransformer transformer BulkImports::Groups::Transformers::SubgroupToEntityTransformer loader BulkImports::Common::Loaders::EntityLoader end diff --git a/lib/bulk_imports/importers/group_importer.rb b/lib/bulk_imports/importers/group_importer.rb index c7253590c87..82cb1ca03a2 100644 --- a/lib/bulk_imports/importers/group_importer.rb +++ b/lib/bulk_imports/importers/group_importer.rb @@ -19,6 +19,7 @@ module BulkImports ) BulkImports::Groups::Pipelines::GroupPipeline.new.run(context) + 'BulkImports::EE::Groups::Pipelines::EpicsPipeline'.constantize.new.run(context) if Gitlab.ee? BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline.new.run(context) entity.finish! diff --git a/lib/bulk_imports/pipeline.rb b/lib/bulk_imports/pipeline.rb index 70e6030ea2c..a44f8fc7193 100644 --- a/lib/bulk_imports/pipeline.rb +++ b/lib/bulk_imports/pipeline.rb @@ -3,10 +3,89 @@ module BulkImports module Pipeline extend ActiveSupport::Concern + include Gitlab::ClassAttributes included do - include Attributes include Runner + + private + + def extractors + @extractors ||= self.class.extractors.map(&method(:instantiate)) + end + + def transformers + @transformers ||= self.class.transformers.map(&method(:instantiate)) + end + + def loaders + @loaders ||= self.class.loaders.map(&method(:instantiate)) + end + + def after_run + @after_run ||= self.class.after_run_callback + end + + def pipeline + @pipeline ||= self.class.name + end + + def instantiate(class_config) + class_config[:klass].new(class_config[:options]) + end + + def abort_on_failure? + self.class.abort_on_failure? + end + end + + class_methods do + def extractor(klass, options = nil) + add_attribute(:extractors, klass, options) + end + + def transformer(klass, options = nil) + add_attribute(:transformers, klass, options) + end + + def loader(klass, options = nil) + add_attribute(:loaders, klass, options) + end + + def after_run(&block) + class_attributes[:after_run] = block + end + + def extractors + class_attributes[:extractors] + end + + def transformers + class_attributes[:transformers] + end + + def loaders + class_attributes[:loaders] + end + + def after_run_callback + class_attributes[:after_run] + end + + def abort_on_failure! + class_attributes[:abort_on_failure] = true + end + + def abort_on_failure? + class_attributes[:abort_on_failure] + end + + private + + def add_attribute(sym, klass, options) + class_attributes[sym] ||= [] + class_attributes[sym] << { klass: klass, options: options } + end end end end diff --git a/lib/bulk_imports/pipeline/attributes.rb b/lib/bulk_imports/pipeline/attributes.rb deleted file mode 100644 index ebfbaf6f6ba..00000000000 --- a/lib/bulk_imports/pipeline/attributes.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -module BulkImports - module Pipeline - module Attributes - extend ActiveSupport::Concern - include Gitlab::ClassAttributes - - class_methods do - def extractor(klass, options = nil) - add_attribute(:extractors, klass, options) - end - - def transformer(klass, options = nil) - add_attribute(:transformers, klass, options) - end - - def loader(klass, options = nil) - add_attribute(:loaders, klass, options) - end - - def add_attribute(sym, klass, options) - class_attributes[sym] ||= [] - class_attributes[sym] << { klass: klass, options: options } - end - - def extractors - class_attributes[:extractors] - end - - def transformers - class_attributes[:transformers] - end - - def loaders - class_attributes[:loaders] - end - end - end - end -end diff --git a/lib/bulk_imports/pipeline/runner.rb b/lib/bulk_imports/pipeline/runner.rb index 04038e50399..88b96f0ab6e 100644 --- a/lib/bulk_imports/pipeline/runner.rb +++ b/lib/bulk_imports/pipeline/runner.rb @@ -5,57 +5,102 @@ module BulkImports module Runner extend ActiveSupport::Concern - included do - private + MarkedAsFailedError = Class.new(StandardError) - def extractors - @extractors ||= self.class.extractors.map(&method(:instantiate)) - end + def run(context) + raise MarkedAsFailedError if marked_as_failed?(context) - def transformers - @transformers ||= self.class.transformers.map(&method(:instantiate)) - end + info(context, message: 'Pipeline started', pipeline_class: pipeline) - def loaders - @loaders ||= self.class.loaders.map(&method(:instantiate)) - end + extractors.each do |extractor| + data = run_pipeline_step(:extractor, extractor.class.name, context) do + extractor.extract(context) + end - def pipeline_name - @pipeline ||= self.class.name - end + if data && data.respond_to?(:each) + data.each do |entry| + transformers.each do |transformer| + entry = run_pipeline_step(:transformer, transformer.class.name, context) do + transformer.transform(context, entry) + end + end - def instantiate(class_config) - class_config[:klass].new(class_config[:options]) + loaders.each do |loader| + run_pipeline_step(:loader, loader.class.name, context) do + loader.load(context, entry) + end + end + end + end end + + after_run.call(context) if after_run.present? + rescue MarkedAsFailedError + log_skip(context) end - def run(context) - info(context, message: "Pipeline started", pipeline: pipeline_name) + private # rubocop:disable Lint/UselessAccessModifier - extractors.each do |extractor| - extractor.extract(context).each do |entry| - info(context, extractor: extractor.class.name) + def run_pipeline_step(type, class_name, context) + raise MarkedAsFailedError if marked_as_failed?(context) - transformers.each do |transformer| - info(context, transformer: transformer.class.name) - entry = transformer.transform(context, entry) - end + info(context, type => class_name) - loaders.each do |loader| - info(context, loader: loader.class.name) - loader.load(context, entry) - end - end - end + yield + rescue MarkedAsFailedError + log_skip(context, type => class_name) + rescue => e + log_import_failure(e, context) + + mark_as_failed(context) if abort_on_failure? end - private # rubocop:disable Lint/UselessAccessModifier + def mark_as_failed(context) + warn(context, message: 'Pipeline failed', pipeline_class: pipeline) + + context.entity.fail_op! + end + + def marked_as_failed?(context) + return true if context.entity.failed? + + false + end + + def log_skip(context, extra = {}) + log = { + message: 'Skipping due to failed pipeline status', + pipeline_class: pipeline + }.merge(extra) + + info(context, log) + end + + def log_import_failure(exception, context) + attributes = { + bulk_import_entity_id: context.entity.id, + pipeline_class: pipeline, + exception_class: exception.class.to_s, + exception_message: exception.message.truncate(255), + correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id + } + + BulkImports::Failure.create(attributes) + end + + def warn(context, extra = {}) + logger.warn(log_base_params(context).merge(extra)) + end def info(context, extra = {}) - logger.info({ - entity: context.entity.id, - entity_type: context.entity.source_type - }.merge(extra)) + logger.info(log_base_params(context).merge(extra)) + end + + def log_base_params(context) + { + bulk_import_entity_id: context.entity.id, + bulk_import_entity_type: context.entity.source_type + } end def logger diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index 3e9cf2ab320..d41490d2ebd 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -4,7 +4,7 @@ module Constraints class ProjectUrlConstrainer def matches?(request, existence_check: true) namespace_path = request.params[:namespace_id] - project_path = request.params[:project_id] || request.params[:id] || request.params[:repository_id] + project_path = request.params[:project_id] || request.params[:id] full_path = [namespace_path, project_path].join('/') return false unless ProjectPathValidator.valid_path?(full_path) diff --git a/lib/constraints/repository_redirect_url_constrainer.rb b/lib/constraints/repository_redirect_url_constrainer.rb new file mode 100644 index 00000000000..44df670d8d3 --- /dev/null +++ b/lib/constraints/repository_redirect_url_constrainer.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Constraints + class RepositoryRedirectUrlConstrainer + def matches?(request) + path = request.params[:repository_path].delete_suffix('.git') + query = request.query_string + + git_request?(query) && container_path?(path) + end + + # Allow /info/refs, /info/refs?service=git-upload-pack, and + # /info/refs?service=git-receive-pack, but nothing else. + def git_request?(query) + query.blank? || + query == 'service=git-upload-pack' || + query == 'service=git-receive-pack' + end + + # Check if the path matches any known repository containers. + # These also cover wikis, since a `.wiki` suffix is valid in project/group paths too. + def container_path?(path) + NamespacePathValidator.valid_path?(path) || + ProjectPathValidator.valid_path?(path) || + path =~ Gitlab::PathRegex.full_snippets_repository_path_regex + end + end +end diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb index 34511423d4a..d130a9d6f82 100644 --- a/lib/extracts_ref.rb +++ b/lib/extracts_ref.rb @@ -62,8 +62,7 @@ module ExtractsRef # # rubocop:disable Gitlab/ModuleWithInstanceVariables def assign_ref_vars - @id = get_id - @ref, @path = extract_ref(@id) + @id, @ref, @path = extract_ref_path @repo = repository_container.repository raise InvalidPathError if @ref.match?(/\s/) @@ -76,6 +75,13 @@ module ExtractsRef @tree ||= @repo.tree(@commit.id, @path) # rubocop:disable Gitlab/ModuleWithInstanceVariables end + def extract_ref_path + id = get_id + ref, path = extract_ref(id) + + [id, ref, path] + end + private def extract_raw_ref(id) diff --git a/lib/feature.rb b/lib/feature.rb index 1f8c530bee5..3d4a919b043 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -68,6 +68,9 @@ class Feature Feature::Definition.valid_usage!(key, type: type, default_enabled: default_enabled) end + # If `default_enabled: :yaml` we fetch the value from the YAML definition instead. + default_enabled = Feature::Definition.default_enabled?(key) if default_enabled == :yaml + # During setup the database does not exist yet. So we haven't stored a value # for the feature yet and return the default. return default_enabled unless Gitlab::Database.exists? @@ -87,40 +90,39 @@ class Feature end def enable(key, thing = true) + log(key: key, action: __method__, thing: thing) get(key).enable(thing) end def disable(key, thing = false) + log(key: key, action: __method__, thing: thing) get(key).disable(thing) end - def enable_group(key, group) - get(key).enable_group(group) - end - - def disable_group(key, group) - get(key).disable_group(group) - end - def enable_percentage_of_time(key, percentage) + log(key: key, action: __method__, percentage: percentage) get(key).enable_percentage_of_time(percentage) end def disable_percentage_of_time(key) + log(key: key, action: __method__) get(key).disable_percentage_of_time end def enable_percentage_of_actors(key, percentage) + log(key: key, action: __method__, percentage: percentage) get(key).enable_percentage_of_actors(percentage) end def disable_percentage_of_actors(key) + log(key: key, action: __method__) get(key).disable_percentage_of_actors end def remove(key) return unless persisted_name?(key) + log(key: key, action: __method__) get(key).remove end @@ -136,8 +138,6 @@ class Feature end def register_definitions - return unless check_feature_flags_definition? - Feature::Definition.reload! end @@ -147,6 +147,10 @@ class Feature Feature::Definition.register_hot_reloader! end + def logger + @logger ||= Feature::Logger.build + end + private def flipper @@ -194,6 +198,14 @@ class Feature def l2_cache_backend Rails.cache end + + def log(key:, action:, **extra) + extra ||= {} + extra = extra.transform_keys { |k| "extra.#{k}" } + extra = extra.transform_values { |v| v.respond_to?(:flipper_id) ? v.flipper_id : v } + extra = extra.transform_values(&:to_s) + logger.info(key: key, action: action, **extra) + end end class Target diff --git a/lib/feature/definition.rb b/lib/feature/definition.rb index 0ba1bdc4799..8d9b2fa5234 100644 --- a/lib/feature/definition.rb +++ b/lib/feature/definition.rb @@ -13,6 +13,12 @@ class Feature end end + TYPES.each do |type, _| + define_method("#{type}?") do + attributes[:type].to_sym == type + end + end + def initialize(path, opts = {}) @path = path @attributes = {} @@ -65,9 +71,7 @@ class Feature "a valid syntax: #{TYPES.dig(type, :example)}" end - # We accept an array of defaults as some features are undefined - # and have `default_enabled: true/false` - unless Array(default_enabled).include?(default_enabled_in_code) + unless default_enabled_in_code == :yaml || default_enabled == default_enabled_in_code # Raise exception in test and dev raise Feature::InvalidFeatureFlagError, "The `default_enabled:` of `#{key}` is not equal to config: " \ "#{default_enabled_in_code} vs #{default_enabled}. Ensure to update #{path}" @@ -90,12 +94,20 @@ class Feature @definitions ||= load_all! end + def get(key) + definitions[key.to_sym] + end + def reload! @definitions = load_all! end + def has_definition?(key) + definitions.has_key?(key.to_sym) + end + def valid_usage!(key, type:, default_enabled:) - if definition = definitions[key.to_sym] + if definition = get(key) definition.valid_usage!(type_in_code: type, default_enabled_in_code: default_enabled) elsif type_definition = self::TYPES[type] raise InvalidFeatureFlagError, "Missing feature definition for `#{key}`" unless type_definition[:optional] @@ -104,6 +116,17 @@ class Feature end end + def default_enabled?(key) + if definition = get(key) + definition.default_enabled + else + Gitlab::ErrorTracking.track_and_raise_for_dev_exception( + InvalidFeatureFlagError.new("The feature flag YAML definition for '#{key}' does not exist")) + + false + end + end + def register_hot_reloader! # Reload feature flags on change of this file or any `.yml` file_watcher = Rails.configuration.file_watcher.new(reload_files, reload_directories) do @@ -119,10 +142,6 @@ class Feature private def load_all! - # We currently do not load feature flag definitions - # in production environments - return [] unless Gitlab.dev_or_test_env? - paths.each_with_object({}) do |glob_path, definitions| load_all_from_path!(definitions, glob_path) end diff --git a/lib/feature/logger.rb b/lib/feature/logger.rb new file mode 100644 index 00000000000..784a619e182 --- /dev/null +++ b/lib/feature/logger.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Feature + class Logger < ::Gitlab::JsonLogger + def self.file_name_noext + 'features_json' + end + end +end diff --git a/lib/feature/shared.rb b/lib/feature/shared.rb index 1fcbc8fa173..17dfe26bd82 100644 --- a/lib/feature/shared.rb +++ b/lib/feature/shared.rb @@ -23,7 +23,7 @@ class Feature example: <<-EOS Feature.enabled?(:my_feature_flag, project) Feature.enabled?(:my_feature_flag, project, type: :development) - push_frontend_feature_flag?(:my_feature_flag, project) + push_frontend_feature_flag(:my_feature_flag, project) EOS }, ops: { @@ -33,8 +33,8 @@ class Feature ee_only: false, default_enabled: false, example: <<-EOS - Feature.enabled?(:my_ops_flag, type: ops) - push_frontend_feature_flag?(:my_ops_flag, project, type: :ops) + Feature.enabled?(:my_ops_flag, type: :ops) + push_frontend_feature_flag(:my_ops_flag, project, type: :ops) EOS }, licensed: { @@ -48,6 +48,16 @@ class Feature project.feature_available?(:my_licensed_feature) namespace.feature_available?(:my_licensed_feature) EOS + }, + experiment: { + description: 'Short lived, used specifically to run A/B/n experiments.', + optional: true, + rollout_issue: true, + ee_only: true, + default_enabled: false, + example: <<-EOS + experiment(:my_experiment, project: project, actor: current_user) { ...variant code... } + EOS } }.freeze diff --git a/lib/gitlab.rb b/lib/gitlab.rb index 43785d165fb..0f2fd01e3c7 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -115,4 +115,10 @@ module Gitlab 'web' end + + def self.maintenance_mode? + return false unless ::Feature.enabled?(:maintenance_mode) + + ::Gitlab::CurrentSettings.maintenance_mode + end end diff --git a/lib/gitlab/alert_management/payload.rb b/lib/gitlab/alert_management/payload.rb index 177d544d720..ce09ffd87ee 100644 --- a/lib/gitlab/alert_management/payload.rb +++ b/lib/gitlab/alert_management/payload.rb @@ -4,7 +4,8 @@ module Gitlab module AlertManagement module Payload MONITORING_TOOLS = { - prometheus: 'Prometheus' + prometheus: 'Prometheus', + cilium: 'Cilium' }.freeze class << self diff --git a/lib/gitlab/analytics/cycle_analytics/default_stages.rb b/lib/gitlab/analytics/cycle_analytics/default_stages.rb index fc91dd6e138..22aa680cbc1 100644 --- a/lib/gitlab/analytics/cycle_analytics/default_stages.rb +++ b/lib/gitlab/analytics/cycle_analytics/default_stages.rb @@ -22,6 +22,10 @@ module Gitlab ] end + def self.find_by_name!(name) + all.find { |raw_stage| raw_stage[:name].to_s.eql?(name.to_s) } || raise("Default stage '#{name}' not found") + end + def self.names all.map { |stage| stage[:name] } end diff --git a/lib/gitlab/application_context.rb b/lib/gitlab/application_context.rb index b4bbb309c36..84fe3d1c959 100644 --- a/lib/gitlab/application_context.rb +++ b/lib/gitlab/application_context.rb @@ -30,7 +30,7 @@ module Gitlab Labkit::Context.current.to_h.include?(Labkit::Context.log_key(attribute_name)) end - def initialize(args) + def initialize(**args) unknown_attributes = args.keys - APPLICATION_ATTRIBUTES.map(&:name) raise ArgumentError, "#{unknown_attributes} are not known keys" if unknown_attributes.any? diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb index e92bbe4f529..fbba86d1253 100644 --- a/lib/gitlab/application_rate_limiter.rb +++ b/lib/gitlab/application_rate_limiter.rb @@ -34,7 +34,8 @@ module Gitlab group_testing_hook: { threshold: 5, interval: 1.minute }, profile_add_new_email: { threshold: 5, interval: 1.minute }, profile_resend_email_confirmation: { threshold: 5, interval: 1.minute }, - update_environment_canary_ingress: { threshold: 1, interval: 1.minute } + update_environment_canary_ingress: { threshold: 1, interval: 1.minute }, + auto_rollback_deployment: { threshold: 1, interval: 3.minutes } }.freeze end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index 5cacd7e5983..a9c2dd001cb 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -2,6 +2,7 @@ require 'asciidoctor' require 'asciidoctor-plantuml' +require 'asciidoctor/extensions/asciidoctor_kroki/extension' require 'asciidoctor/extensions' require 'gitlab/asciidoc/html5_converter' require 'gitlab/asciidoc/mermaid_block_processor' @@ -23,7 +24,14 @@ module Gitlab 'source-highlighter' => 'gitlab-html-pipeline', 'icons' => 'font', 'outfilesuffix' => '.adoc', - 'max-include-depth' => MAX_INCLUDE_DEPTH + 'max-include-depth' => MAX_INCLUDE_DEPTH, + # This feature is disabled because it relies on File#read to read the file. + # If we want to enable this feature we will need to provide a "GitLab compatible" implementation. + # This attribute is typically used to share common config (skinparam...) across all PlantUML diagrams. + # The value can be a path or a URL. + 'kroki-plantuml-include!' => '', + # This feature is disabled because it relies on the local file system to save diagrams retrieved from the Kroki server. + 'kroki-fetch-diagram!' => '' }.freeze def self.path_attrs(path) @@ -48,12 +56,21 @@ module Gitlab extensions = proc do include_processor ::Gitlab::Asciidoc::IncludeProcessor.new(context) block ::Gitlab::Asciidoc::MermaidBlockProcessor + ::Gitlab::Kroki.formats(Gitlab::CurrentSettings).each do |name| + block ::AsciidoctorExtensions::KrokiBlockProcessor, name + end end extra_attrs = path_attrs(context[:requested_path]) asciidoc_opts = { safe: :secure, backend: :gitlab_html5, - attributes: DEFAULT_ADOC_ATTRS.merge(extra_attrs), + attributes: DEFAULT_ADOC_ATTRS + .merge(extra_attrs) + .merge({ + # Define the Kroki server URL from the settings. + # This attribute cannot be overridden from the AsciiDoc document. + 'kroki-server-url' => Gitlab::CurrentSettings.kroki_url + }), extensions: extensions } context[:pipeline] = :ascii_doc diff --git a/lib/gitlab/asciidoc/html5_converter.rb b/lib/gitlab/asciidoc/html5_converter.rb index e5163e1954c..787eab27503 100644 --- a/lib/gitlab/asciidoc/html5_converter.rb +++ b/lib/gitlab/asciidoc/html5_converter.rb @@ -19,6 +19,12 @@ module Gitlab %(<code#{id_attribute(node)} data-math-style="inline">#{node.text}</code>) end + def convert_inline_anchor(node) + node.id = "user-content-#{node.id}" if node.id && !node.id.start_with?('user-content-') + + super(node) + end + private def id_attribute(node) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index fadd6eb848d..1aabb05f19e 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -196,11 +196,9 @@ module Gitlab return unless token - return if project && token.user.project_bot? && !project.bots.include?(token.user) - return unless valid_scoped_token?(token, all_available_scopes) - if token.user.project_bot? || token.user.can?(:log_in) + if token.user.can?(:log_in) || token.user.can?(:bot_log_in, project) Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes)) end end @@ -285,7 +283,7 @@ module Gitlab return unless build.project.builds_enabled? if build.user - return unless build.user.can?(:log_in) + return unless build.user.can?(:log_in) || build.user.can?(:bot_log_in, build.project) # If user is assigned to build, use restricted credentials of user Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities) diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb index f3975fe219a..caa881eeeab 100644 --- a/lib/gitlab/auth/auth_finders.rb +++ b/lib/gitlab/auth/auth_finders.rb @@ -46,6 +46,7 @@ module Gitlab def find_user_from_feed_token(request_format) return unless valid_rss_format?(request_format) + return if Gitlab::CurrentSettings.disable_feed_token # NOTE: feed_token was renamed from rss_token but both needs to be supported because # users might have already added the feed to their RSS reader before the rename @@ -193,6 +194,10 @@ module Gitlab def access_token strong_memoize(:access_token) do + # The token can be a PAT or an OAuth (doorkeeper) token + # It is also possible that a PAT is encapsulated in a `Bearer` OAuth token + # (e.g. NPM client registry auth), this case will be properly handled + # by find_personal_access_token find_oauth_access_token || find_personal_access_token end end @@ -236,7 +241,7 @@ module Gitlab end def matches_personal_access_token_length?(token) - token.length == PersonalAccessToken::TOKEN_LENGTH + PersonalAccessToken::TOKEN_LENGTH_RANGE.include?(token.length) end # Check if the request is GET/HEAD, or if CSRF token is valid. diff --git a/lib/gitlab/auth/crowd/authentication.rb b/lib/gitlab/auth/crowd/authentication.rb new file mode 100644 index 00000000000..7f3e980034e --- /dev/null +++ b/lib/gitlab/auth/crowd/authentication.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Crowd + class Authentication < Gitlab::Auth::OAuth::Authentication + def login(login, password) + return unless Gitlab::Auth::OAuth::Provider.enabled?(@provider) + return unless login.present? && password.present? + + user_info = user_info_from_authentication(login, password) + return unless user_info&.key?(:user) + + Gitlab::Auth::OAuth::User.find_by_uid_and_provider(user_info[:user], provider) + end + + private + + def config + gitlab_crowd_config = Gitlab::Auth::OAuth::Provider.config_for(@provider) + raise "OmniAuth Crowd is not configured." unless gitlab_crowd_config && gitlab_crowd_config[:args] + + OmniAuth::Strategies::Crowd::Configuration.new( + gitlab_crowd_config[:args].symbolize_keys) + end + + def user_info_from_authentication(login, password) + validator = OmniAuth::Strategies::Crowd::CrowdValidator.new( + config, login, password, RequestContext.instance.client_ip, nil) + validator&.user_info&.symbolize_keys + end + end + end + end +end diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb index 88cc840c395..f5931a1d5eb 100644 --- a/lib/gitlab/auth/ldap/config.rb +++ b/lib/gitlab/auth/ldap/config.rb @@ -53,6 +53,10 @@ module Gitlab raise InvalidProvider.new("Unknown provider (#{provider}). Available providers: #{providers}") end + def self.encrypted_secrets + Settings.encrypted(Gitlab.config.ldap.secret_file) + end + def initialize(provider) if self.class.valid_provider?(provider) @provider = provider @@ -89,8 +93,8 @@ module Gitlab if has_auth? opts.merge!( - bind_dn: options['bind_dn'], - password: options['password'] + bind_dn: auth_username, + password: auth_password ) end @@ -155,7 +159,7 @@ module Gitlab end def has_auth? - options['password'] || options['bind_dn'] + auth_password || auth_username end def allow_username_or_email_login @@ -267,12 +271,32 @@ module Gitlab { auth: { method: :simple, - username: options['bind_dn'], - password: options['password'] + username: auth_username, + password: auth_password } } end + def secrets + @secrets ||= self.class.encrypted_secrets[@provider.delete_prefix('ldap').to_sym] + rescue => e + Gitlab::AppLogger.error "LDAP encrypted secrets are invalid: #{e.inspect}" + + nil + end + + def auth_password + return options['password'] if options['password'] + + secrets&.fetch(:password, nil)&.chomp + end + + def auth_username + return options['bind_dn'] if options['bind_dn'] + + secrets&.fetch(:bind_dn, nil)&.chomp + end + def omniauth_user_filter uid_filter = Net::LDAP::Filter.eq(uid, '%{username}') diff --git a/lib/gitlab/auth/ldap/user.rb b/lib/gitlab/auth/ldap/user.rb index 1405fb4ab95..814c17b7e44 100644 --- a/lib/gitlab/auth/ldap/user.rb +++ b/lib/gitlab/auth/ldap/user.rb @@ -11,16 +11,6 @@ module Gitlab module Ldap class User < Gitlab::Auth::OAuth::User extend ::Gitlab::Utils::Override - class << self - # rubocop: disable CodeReuse/ActiveRecord - def find_by_uid_and_provider(uid, provider) - identity = ::Identity.with_extern_uid(provider, uid).take - - identity && identity.user - end - # rubocop: enable CodeReuse/ActiveRecord - end - def save super('LDAP') end @@ -30,10 +20,6 @@ module Gitlab find_by_uid_and_provider || find_by_email || build_new_user end - def find_by_uid_and_provider - self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider) - end - override :should_save? def should_save? gl_user.changed? || gl_user.identities.any?(&:changed?) diff --git a/lib/gitlab/auth/o_auth/provider.rb b/lib/gitlab/auth/o_auth/provider.rb index 1eae7af442d..57ff3fcd1f0 100644 --- a/lib/gitlab/auth/o_auth/provider.rb +++ b/lib/gitlab/auth/o_auth/provider.rb @@ -18,6 +18,8 @@ module Gitlab authenticator = case provider + when /crowd/ + Gitlab::Auth::Crowd::Authentication when /^ldap/ Gitlab::Auth::Ldap::Authentication when 'database' diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb index 3211d2ffaea..f556a7f40e9 100644 --- a/lib/gitlab/auth/o_auth/user.rb +++ b/lib/gitlab/auth/o_auth/user.rb @@ -9,6 +9,16 @@ module Gitlab module Auth module OAuth class User + class << self + # rubocop: disable CodeReuse/ActiveRecord + def find_by_uid_and_provider(uid, provider) + identity = ::Identity.with_extern_uid(provider, uid).take + + identity && identity.user + end + # rubocop: enable CodeReuse/ActiveRecord + end + SignupDisabledError = Class.new(StandardError) SigninDisabledForProviderError = Class.new(StandardError) @@ -190,15 +200,12 @@ module Gitlab @auth_hash = AuthHash.new(auth_hash) end - # rubocop: disable CodeReuse/ActiveRecord def find_by_uid_and_provider - identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take - identity&.user + self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider) end - # rubocop: enable CodeReuse/ActiveRecord - def build_new_user - user_params = user_attributes.merge(skip_confirmation: true) + def build_new_user(skip_confirmation: true) + user_params = user_attributes.merge(skip_confirmation: skip_confirmation) Users::BuildService.new(nil, user_params).execute(skip_authorization: true) end diff --git a/lib/gitlab/auth/otp/fortinet.rb b/lib/gitlab/auth/otp/fortinet.rb new file mode 100644 index 00000000000..a561e97dfcd --- /dev/null +++ b/lib/gitlab/auth/otp/fortinet.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +module Gitlab + module Auth + module Otp + module Fortinet + private + + def forti_authenticator_enabled?(user) + ::Gitlab.config.forti_authenticator.enabled && + Feature.enabled?(:forti_authenticator, user) + end + + def forti_token_cloud_enabled?(user) + ::Gitlab.config.forti_token_cloud.enabled && + Feature.enabled?(:forti_token_cloud, user) + end + end + end + end +end diff --git a/lib/gitlab/auth/otp/session_enforcer.rb b/lib/gitlab/auth/otp/session_enforcer.rb new file mode 100644 index 00000000000..8cc280756cc --- /dev/null +++ b/lib/gitlab/auth/otp/session_enforcer.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Otp + class SessionEnforcer + OTP_SESSIONS_NAMESPACE = 'session:otp' + DEFAULT_EXPIRATION = 15.minutes.to_i + + def initialize(key) + @key = key + end + + def update_session + Gitlab::Redis::SharedState.with do |redis| + redis.setex(key_name, DEFAULT_EXPIRATION, true) + end + end + + def access_restricted? + Gitlab::Redis::SharedState.with do |redis| + !redis.get(key_name) + end + end + + private + + attr_reader :key + + def key_name + @key_name ||= "#{OTP_SESSIONS_NAMESPACE}:#{key.id}" + end + end + end + end +end diff --git a/lib/gitlab/auth/otp/strategies/base.rb b/lib/gitlab/auth/otp/strategies/base.rb index 718630e0e31..7d8513642c4 100644 --- a/lib/gitlab/auth/otp/strategies/base.rb +++ b/lib/gitlab/auth/otp/strategies/base.rb @@ -25,6 +25,10 @@ module Gitlab result end + + def error_from_response(response) + error(response.message, response.code) + end end end end diff --git a/lib/gitlab/auth/otp/strategies/forti_authenticator.rb b/lib/gitlab/auth/otp/strategies/forti_authenticator.rb index fbcb9fd8cdb..c1433f05db2 100644 --- a/lib/gitlab/auth/otp/strategies/forti_authenticator.rb +++ b/lib/gitlab/auth/otp/strategies/forti_authenticator.rb @@ -17,7 +17,10 @@ module Gitlab # Successful authentication results in HTTP 200: OK # https://docs.fortinet.com/document/fortiauthenticator/6.2.0/rest-api-solution-guide/704555/authentication-auth - response.ok? ? success : error(message: response.message, http_status: response.code) + response.ok? ? success : error_from_response(response) + rescue StandardError => ex + Gitlab::AppLogger.error(ex) + error(ex.message) end private @@ -32,7 +35,7 @@ module Gitlab def api_credentials { username: ::Gitlab.config.forti_authenticator.username, - password: ::Gitlab.config.forti_authenticator.token } + password: ::Gitlab.config.forti_authenticator.access_token } end end end diff --git a/lib/gitlab/auth/otp/strategies/forti_token_cloud.rb b/lib/gitlab/auth/otp/strategies/forti_token_cloud.rb new file mode 100644 index 00000000000..d7506eca242 --- /dev/null +++ b/lib/gitlab/auth/otp/strategies/forti_token_cloud.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Otp + module Strategies + class FortiTokenCloud < Base + include Gitlab::Utils::StrongMemoize + BASE_API_URL = 'https://ftc.fortinet.com:9696/api/v1' + + def validate(otp_code) + if access_token_create_response.created? + otp_verification_response = verify_otp(otp_code) + + otp_verification_response.ok? ? success : error_from_response(otp_verification_response) + else + error_from_response(access_token_create_response) + end + end + + private + + # TODO: Cache the access token: https://gitlab.com/gitlab-org/gitlab/-/issues/292437 + def access_token_create_response + # Returns '201 CREATED' on successful creation of a new access token. + strong_memoize(:access_token_create_response) do + post( + url: url('/login'), + body: { + client_id: ::Gitlab.config.forti_token_cloud.client_id, + client_secret: ::Gitlab.config.forti_token_cloud.client_secret + }.to_json + ) + end + end + + def access_token + Gitlab::Json.parse(access_token_create_response)['access_token'] + end + + def verify_otp(otp_code) + # Returns '200 OK' on successful verification. + # Uses the access token created via `access_token_create_response` as the auth token. + post( + url: url('/auth'), + headers: { 'Authorization': "Bearer #{access_token}" }, + body: { + username: user.username, + token: otp_code + }.to_json + ) + end + + def url(path) + BASE_API_URL + path + end + + def post(url:, body:, headers: {}) + Gitlab::HTTP.post( + url, + headers: { + 'Content-Type': 'application/json' + }.merge(headers), + body: body, + verify: false # FTC API Docs specifically mentions to turn off SSL Verification while making requests. + ) + end + end + end + end + end +end diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb index c6216fa9cad..d28ee54cfbc 100644 --- a/lib/gitlab/auth/request_authenticator.rb +++ b/lib/gitlab/auth/request_authenticator.rb @@ -49,9 +49,16 @@ module Gitlab private + def access_token + strong_memoize(:access_token) do + super || find_personal_access_token_from_http_basic_auth + end + end + def route_authentication_setting @route_authentication_setting ||= { - job_token_allowed: api_request? + job_token_allowed: api_request?, + basic_auth_personal_access_token: api_request? } end end diff --git a/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities.rb b/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities.rb new file mode 100644 index 00000000000..68c91650d93 --- /dev/null +++ b/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This class updates vulnerabilities entities with state dismissed + class PopulateDismissedStateForVulnerabilities + class Vulnerability < ActiveRecord::Base # rubocop:disable Style/Documentation + self.table_name = 'vulnerabilities' + end + + def perform(*vulnerability_ids) + Vulnerability.where(id: vulnerability_ids).update_all(state: 2) + PopulateMissingVulnerabilityDismissalInformation.new.perform(*vulnerability_ids) + end + end + end +end diff --git a/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information.rb b/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information.rb index bc0a181a06c..04342fdabd4 100644 --- a/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information.rb +++ b/lib/gitlab/background_migration/populate_missing_vulnerability_dismissal_information.rb @@ -26,13 +26,16 @@ module Gitlab class Finding < ActiveRecord::Base # rubocop:disable Style/Documentation include ShaAttribute + include ::Gitlab::Utils::StrongMemoize self.table_name = 'vulnerability_occurrences' sha_attribute :project_fingerprint def dismissal_feedback - Feedback.dismissal.where(category: report_type, project_fingerprint: project_fingerprint, project_id: project_id).first + strong_memoize(:dismissal_feedback) do + Feedback.dismissal.where(category: report_type, project_fingerprint: project_fingerprint, project_id: project_id).first + end end end diff --git a/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb b/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb index a0c89cc4664..2e81b1615d8 100644 --- a/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb +++ b/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb @@ -5,7 +5,7 @@ module Gitlab # This class creates/updates those project historical vulnerability statistics # that haven't been created nor initialized. It should only be executed in EE. class PopulateVulnerabilityHistoricalStatistics - def perform(project_ids) + def perform(project_ids, retention_period = 90) end end end diff --git a/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth.rb b/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth.rb new file mode 100644 index 00000000000..d97765cd398 --- /dev/null +++ b/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class UpdateExistingUsersThatRequireTwoFactorAuth # rubocop:disable Metrics/ClassLength + def perform(start_id, stop_id) + ActiveRecord::Base.connection.execute <<~SQL + UPDATE + users + SET + require_two_factor_authentication_from_group = FALSE + WHERE + users.id BETWEEN #{start_id} + AND #{stop_id} + AND users.require_two_factor_authentication_from_group = TRUE + AND users.id NOT IN ( SELECT DISTINCT + users_groups_query.user_id + FROM ( + SELECT + users.id AS user_id, + members.source_id AS group_ids + FROM + users + LEFT JOIN members ON members.source_type = 'Namespace' + AND members.requested_at IS NULL + AND members.user_id = users.id + AND members.type = 'GroupMember' + WHERE + users.require_two_factor_authentication_from_group = TRUE + AND users.id BETWEEN #{start_id} + AND #{stop_id}) AS users_groups_query + INNER JOIN LATERAL ( WITH RECURSIVE "base_and_ancestors" AS ( + ( + SELECT + "namespaces"."type", + "namespaces"."id", + "namespaces"."parent_id", + "namespaces"."require_two_factor_authentication" + FROM + "namespaces" + WHERE + "namespaces"."type" = 'Group' + AND "namespaces"."id" = users_groups_query.group_ids) + UNION ( + SELECT + "namespaces"."type", + "namespaces"."id", + "namespaces"."parent_id", + "namespaces"."require_two_factor_authentication" + FROM + "namespaces", + "base_and_ancestors" + WHERE + "namespaces"."type" = 'Group' + AND "namespaces"."id" = "base_and_ancestors"."parent_id")), + "base_and_descendants" AS ( + ( + SELECT + "namespaces"."type", + "namespaces"."id", + "namespaces"."parent_id", + "namespaces"."require_two_factor_authentication" + FROM + "namespaces" + WHERE + "namespaces"."type" = 'Group' + AND "namespaces"."id" = users_groups_query.group_ids) + UNION ( + SELECT + "namespaces"."type", + "namespaces"."id", + "namespaces"."parent_id", + "namespaces"."require_two_factor_authentication" + FROM + "namespaces", + "base_and_descendants" + WHERE + "namespaces"."type" = 'Group' + AND "namespaces"."parent_id" = "base_and_descendants"."id")) + SELECT + "namespaces".* + FROM (( + SELECT + "namespaces"."type", + "namespaces"."id", + "namespaces"."parent_id", + "namespaces"."require_two_factor_authentication" + FROM + "base_and_ancestors" AS "namespaces" + WHERE + "namespaces"."type" = 'Group') + UNION ( + SELECT + "namespaces"."type", + "namespaces"."id", + "namespaces"."parent_id", + "namespaces"."require_two_factor_authentication" + FROM + "base_and_descendants" AS "namespaces" + WHERE + "namespaces"."type" = 'Group')) namespaces + WHERE + "namespaces"."type" = 'Group' + AND "namespaces"."require_two_factor_authentication" = TRUE) AS hierarchy_tree ON TRUE); + SQL + end + end + end +end diff --git a/lib/gitlab/batch_pop_queueing.rb b/lib/gitlab/batch_pop_queueing.rb index 61011abddf5..e18f1320ea4 100644 --- a/lib/gitlab/batch_pop_queueing.rb +++ b/lib/gitlab/batch_pop_queueing.rb @@ -9,7 +9,7 @@ module Gitlab # and the following items wait until the next items have been popped from the queue. # On the other hand, this queueing system, the former part is same, however, # it pops the enqueued items as batch. This is especially useful when you want to - # drop redandant items from the queue in order to process important items only, + # drop redundant items from the queue in order to process important items only, # thus it's more efficient than the traditional queueing system. # # Caveats: diff --git a/lib/gitlab/batch_worker_context.rb b/lib/gitlab/batch_worker_context.rb index 0589206fefc..9bc877fcd8d 100644 --- a/lib/gitlab/batch_worker_context.rb +++ b/lib/gitlab/batch_worker_context.rb @@ -23,7 +23,7 @@ module Gitlab def context_by_arguments @context_by_arguments ||= objects.each_with_object({}) do |object, result| arguments = Array.wrap(arguments_proc.call(object)) - context = Gitlab::ApplicationContext.new(context_proc.call(object)) + context = Gitlab::ApplicationContext.new(**context_proc.call(object)) result[arguments] = context end diff --git a/lib/gitlab/checks/diff_check.rb b/lib/gitlab/checks/diff_check.rb index 8780b410a07..c0b228dee59 100644 --- a/lib/gitlab/checks/diff_check.rb +++ b/lib/gitlab/checks/diff_check.rb @@ -17,17 +17,26 @@ module Gitlab file_paths = [] - process_commits do |commit| - validate_once(commit) do - commit.raw_deltas.each do |diff| - file_paths.concat([diff.new_path, diff.old_path].compact) + if ::Feature.enabled?(:diff_check_with_paths_changed_rpc, project, default_enabled: true) + paths = project.repository.find_changed_paths(commits.map(&:sha)) + paths.each do |path| + file_paths.concat([path.path]) - validate_diff(diff) + validate_diff(path) + end + else + process_commits do |commit| + validate_once(commit) do + commit.raw_deltas.each do |diff| + file_paths.concat([diff.new_path, diff.old_path].compact) + + validate_diff(diff) + end end end end - validate_file_paths(file_paths) + validate_file_paths(file_paths.uniq) end private diff --git a/lib/gitlab/checks/push_check.rb b/lib/gitlab/checks/push_check.rb index 7cc5bc56cbb..47aa25aae4c 100644 --- a/lib/gitlab/checks/push_check.rb +++ b/lib/gitlab/checks/push_check.rb @@ -14,7 +14,7 @@ module Gitlab private def can_push? - user_access.can_do_action?(:push_code) || + user_access.can_push_for_ref?(ref) || project.branch_allows_collaboration?(user_access.user, branch_name) end end diff --git a/lib/gitlab/checks/snippet_check.rb b/lib/gitlab/checks/snippet_check.rb index 8c61b782baa..d5efbfcc5bc 100644 --- a/lib/gitlab/checks/snippet_check.rb +++ b/lib/gitlab/checks/snippet_check.rb @@ -10,12 +10,13 @@ module Gitlab ATTRIBUTES = %i[oldrev newrev ref branch_name tag_name logger].freeze attr_reader(*ATTRIBUTES) - def initialize(change, default_branch:, logger:) + def initialize(change, default_branch:, root_ref:, logger:) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @branch_name = Gitlab::Git.branch_name(@ref) @tag_name = Gitlab::Git.tag_name(@ref) @default_branch = default_branch + @root_ref = root_ref @logger = logger @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}") end @@ -26,12 +27,21 @@ module Gitlab end true + rescue GitAccess::ForbiddenError => e + Gitlab::ErrorTracking.log_exception(e, default_branch: @default_branch, branch_name: @branch_name, creation: creation?, deletion: deletion?) + + raise e end private + # If the `root_ref` is not present means that this is the first commit to the + # repository and when the default branch is going to be created. + # We allow the first branch creation no matter the name because + # it can be even an imported snippet from an instance with a different + # default branch. def creation? - @branch_name != @default_branch && super + super && @root_ref && (@branch_name != @default_branch) end end end diff --git a/lib/gitlab/checks/timed_logger.rb b/lib/gitlab/checks/timed_logger.rb index f365e0a43f6..0db38d32bb3 100644 --- a/lib/gitlab/checks/timed_logger.rb +++ b/lib/gitlab/checks/timed_logger.rb @@ -31,7 +31,7 @@ module Gitlab args = { cancelled: true } args[:start] = start if timed - append_message(log_message + time_suffix_message(args)) + append_message(log_message + time_suffix_message(**args)) raise TimeoutError end diff --git a/lib/gitlab/ci/ansi2json/converter.rb b/lib/gitlab/ci/ansi2json/converter.rb index 6d152c052dc..ddf40296809 100644 --- a/lib/gitlab/ci/ansi2json/converter.rb +++ b/lib/gitlab/ci/ansi2json/converter.rb @@ -22,8 +22,7 @@ module Gitlab start_offset = @state.offset - @state.new_line!( - style: Style.new(@state.inherited_style)) + @state.new_line!(style: Style.new(**@state.inherited_style)) stream.each_line do |line| consume_line(line) diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb index a500a0cc35d..a39afee194c 100644 --- a/lib/gitlab/ci/build/rules.rb +++ b/lib/gitlab/ci/build/rules.rb @@ -6,18 +6,31 @@ module Gitlab class Rules include ::Gitlab::Utils::StrongMemoize - Result = Struct.new(:when, :start_in, :allow_failure) do - def build_attributes + Result = Struct.new(:when, :start_in, :allow_failure, :variables) do + def build_attributes(seed_attributes = {}) { when: self.when, options: { start_in: start_in }.compact, - allow_failure: allow_failure + allow_failure: allow_failure, + yaml_variables: yaml_variables(seed_attributes[:yaml_variables]) }.compact end def pass? self.when != 'never' end + + private + + def yaml_variables(seed_variables) + return unless variables && seed_variables + + indexed_seed_variables = seed_variables.deep_dup.index_by { |var| var[:key] } + + variables.each_with_object(indexed_seed_variables) do |var, hash| + hash[var[0].to_s] = { key: var[0].to_s, value: var[1], public: true } + end.values + end end def initialize(rule_hashes, default_when:) @@ -32,7 +45,8 @@ module Gitlab Result.new( matched_rule.attributes[:when] || @default_when, matched_rule.attributes[:start_in], - matched_rule.attributes[:allow_failure] + matched_rule.attributes[:allow_failure], + matched_rule.attributes[:variables] ) else Result.new('never') diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb index cbecce57163..9c2f6eea1dd 100644 --- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb +++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb @@ -11,7 +11,7 @@ module Gitlab def satisfied_by?(pipeline, context) return true if pipeline.modified_paths.nil? - expanded_globs = expand_globs(pipeline, context) + expanded_globs = expand_globs(context) pipeline.modified_paths.any? do |path| expanded_globs.any? do |glob| File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB) @@ -19,8 +19,7 @@ module Gitlab end end - def expand_globs(pipeline, context) - return @globs unless ::Feature.enabled?(:ci_variable_expansion_in_rules_changes, pipeline.project, default_enabled: true) + def expand_globs(context) return @globs unless context @globs.map do |glob| diff --git a/lib/gitlab/ci/config/entry/allow_failure.rb b/lib/gitlab/ci/config/entry/allow_failure.rb new file mode 100644 index 00000000000..de768c3a03b --- /dev/null +++ b/lib/gitlab/ci/config/entry/allow_failure.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents allow_failure settings. + # + class AllowFailure < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Attributable + include ::Gitlab::Config::Entry::Validatable + + ALLOWED_KEYS = %i[exit_codes].freeze + attributes ALLOWED_KEYS + + validations do + validates :config, hash_or_boolean: true + validates :config, allowed_keys: ALLOWED_KEYS + validates :exit_codes, array_of_integers_or_integer: true, allow_nil: true + end + + def value + @config[:exit_codes] = Array.wrap(exit_codes) if exit_codes.present? + @config + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/bridge.rb b/lib/gitlab/ci/config/entry/bridge.rb index 70fcc1d586a..e8e2eef281e 100644 --- a/lib/gitlab/ci/config/entry/bridge.rb +++ b/lib/gitlab/ci/config/entry/bridge.rb @@ -22,6 +22,7 @@ module Gitlab in: ALLOWED_WHEN, message: "should be one of: #{ALLOWED_WHEN.join(', ')}" } + validates :allow_failure, boolean: true end validate on: :composed do @@ -47,7 +48,7 @@ module Gitlab inherit: false, metadata: { allowed_needs: %i[job bridge] } - attributes :when + attributes :when, :allow_failure def self.matching?(name, config) !name.to_s.start_with?('.') && @@ -72,6 +73,10 @@ module Gitlab def bridge_needs needs_value[:bridge] if needs_value end + + def ignored? + allow_failure.nil? ? manual_action? : allow_failure + end end end end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 1ce7060df22..85e3514499c 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -31,6 +31,7 @@ module Gitlab validates :dependencies, array_of_strings: true validates :resource_group, type: String + validates :allow_failure, hash_or_boolean: true end validates :start_in, duration: { limit: '1 week' }, if: :delayed? @@ -117,9 +118,14 @@ module Gitlab description: 'Parallel configuration for this job.', inherit: false + entry :allow_failure, ::Gitlab::Ci::Config::Entry::AllowFailure, + description: 'Indicates whether this job is allowed to fail or not.', + inherit: false + attributes :script, :tags, :when, :dependencies, :needs, :retry, :parallel, :start_in, - :interruptible, :timeout, :resource_group, :release + :interruptible, :timeout, :resource_group, + :release, :allow_failure def self.matching?(name, config) !name.to_s.start_with?('.') && @@ -166,11 +172,32 @@ module Gitlab release: release_value, after_script: after_script_value, ignore: ignored?, + allow_failure_criteria: allow_failure_criteria, needs: needs_defined? ? needs_value : nil, resource_group: resource_group, scheduling_type: needs_defined? ? :dag : :stage ).compact end + + def ignored? + allow_failure_defined? ? static_allow_failure : manual_action? + end + + private + + def allow_failure_criteria + return unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled? + + if allow_failure_defined? && allow_failure_value.is_a?(Hash) + allow_failure_value + end + end + + def static_allow_failure + return false if allow_failure_value.is_a?(Hash) + + allow_failure_value + end end end end diff --git a/lib/gitlab/ci/config/entry/need.rb b/lib/gitlab/ci/config/entry/need.rb index abfffb7a5ed..46191eca842 100644 --- a/lib/gitlab/ci/config/entry/need.rb +++ b/lib/gitlab/ci/config/entry/need.rb @@ -8,7 +8,19 @@ module Gitlab strategy :JobString, if: -> (config) { config.is_a?(String) } strategy :JobHash, - if: -> (config) { config.is_a?(Hash) && config.key?(:job) && !(config.key?(:project) || config.key?(:ref)) } + if: -> (config) { config.is_a?(Hash) && same_pipeline_need?(config) } + + strategy :CrossPipelineDependency, + if: -> (config) { config.is_a?(Hash) && cross_pipeline_need?(config) } + + def self.same_pipeline_need?(config) + config.key?(:job) && + !(config.key?(:project) || config.key?(:ref) || config.key?(:pipeline)) + end + + def self.cross_pipeline_need?(config) + config.key?(:job) && config.key?(:pipeline) && !config.key?(:project) + end class JobString < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable @@ -50,6 +62,30 @@ module Gitlab end end + class CrossPipelineDependency < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + + ALLOWED_KEYS = %i[pipeline job artifacts].freeze + attributes :pipeline, :job, :artifacts + + validations do + validates :config, presence: true + validates :config, allowed_keys: ALLOWED_KEYS + validates :pipeline, type: String, presence: true + validates :job, type: String, presence: true + validates :artifacts, boolean: true, allow_nil: true + end + + def type + :cross_dependency + end + + def value + super.merge(artifacts: artifacts || artifacts.nil?) + end + end + class UnknownStrategy < ::Gitlab::Config::Entry::Node def type end diff --git a/lib/gitlab/ci/config/entry/needs.rb b/lib/gitlab/ci/config/entry/needs.rb index 66cd57b8cf3..dd01cfeedff 100644 --- a/lib/gitlab/ci/config/entry/needs.rb +++ b/lib/gitlab/ci/config/entry/needs.rb @@ -10,6 +10,8 @@ module Gitlab class Needs < ::Gitlab::Config::Entry::ComposableArray include ::Gitlab::Config::Entry::Validatable + NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT = 5 + validations do validate do unless config.is_a?(Hash) || config.is_a?(Array) @@ -27,6 +29,15 @@ module Gitlab errors.add(:config, "uses invalid types: #{extra_keys.join(', ')}") end end + + validate on: :composed do + cross_dependencies = value[:cross_dependency].to_a + cross_pipeline_dependencies = cross_dependencies.select { |dep| dep[:pipeline] } + + if cross_pipeline_dependencies.size > NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT + errors.add(:config, "must be less than or equal to #{NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT}") + end + end end def value diff --git a/lib/gitlab/ci/config/entry/processable.rb b/lib/gitlab/ci/config/entry/processable.rb index c0315e5f901..5ef8cfbddb7 100644 --- a/lib/gitlab/ci/config/entry/processable.rb +++ b/lib/gitlab/ci/config/entry/processable.rb @@ -32,7 +32,6 @@ module Gitlab with_options allow_nil: true do validates :extends, array_of_strings_or_string: true validates :rules, array_of_hashes: true - validates :allow_failure, boolean: true end end @@ -65,7 +64,7 @@ module Gitlab inherit: false, default: {} - attributes :extends, :rules, :allow_failure + attributes :extends, :rules end def compose!(deps = nil) @@ -141,10 +140,6 @@ module Gitlab def manual_action? self.when == 'manual' end - - def ignored? - allow_failure.nil? ? manual_action? : allow_failure - end end end end diff --git a/lib/gitlab/ci/config/entry/root.rb b/lib/gitlab/ci/config/entry/root.rb index 2d93f1ab06e..54ef84b965a 100644 --- a/lib/gitlab/ci/config/entry/root.rb +++ b/lib/gitlab/ci/config/entry/root.rb @@ -50,6 +50,7 @@ module Gitlab entry :variables, Entry::Variables, description: 'Environment variables that will be used.', + metadata: { use_value_data: true }, reserved: true entry :stages, Entry::Stages, diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb index 8ffd49b8a93..840f2d6f31a 100644 --- a/lib/gitlab/ci/config/entry/rules/rule.rb +++ b/lib/gitlab/ci/config/entry/rules/rule.rb @@ -6,14 +6,18 @@ module Gitlab module Entry class Rules::Rule < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Configurable include ::Gitlab::Config::Entry::Attributable CLAUSES = %i[if changes exists].freeze - ALLOWED_KEYS = %i[if changes exists when start_in allow_failure].freeze + ALLOWED_KEYS = %i[if changes exists when start_in allow_failure variables].freeze ALLOWABLE_WHEN = %w[on_success on_failure always never manual delayed].freeze attributes :if, :changes, :exists, :when, :start_in, :allow_failure + entry :variables, Entry::Variables, + description: 'Environment variables to define for rule conditions.' + validations do validates :config, presence: true validates :config, type: { with: Hash } diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb index e258f7128fc..dc164d752be 100644 --- a/lib/gitlab/ci/config/entry/variables.rb +++ b/lib/gitlab/ci/config/entry/variables.rb @@ -13,7 +13,8 @@ module Gitlab ALLOWED_VALUE_DATA = %i[value description].freeze validations do - validates :config, variables: { allowed_value_data: ALLOWED_VALUE_DATA } + validates :config, variables: { allowed_value_data: ALLOWED_VALUE_DATA }, if: :use_value_data? + validates :config, variables: true, unless: :use_value_data? end def value @@ -28,6 +29,10 @@ module Gitlab Hash[@config.map { |key, value| [key.to_s, expand_value(value)] }] end + def use_value_data? + opt(:use_value_data) + end + private def expand_value(value) diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index 661189eea50..af1df933b36 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -55,12 +55,8 @@ module Gitlab ::Feature.enabled?(:ci_trace_log_invalid_chunks, project, type: :ops, default_enabled: false) end - def self.manual_bridges_enabled?(project) - ::Feature.enabled?(:ci_manual_bridges, project, default_enabled: true) - end - - def self.auto_rollback_available?(project) - ::Feature.enabled?(:cd_auto_rollback, project) && project&.feature_available?(:auto_rollback) + def self.pipeline_open_merge_requests?(project) + ::Feature.enabled?(:ci_pipeline_open_merge_requests, project, default_enabled: false) end def self.seed_block_run_before_workflow_rules_enabled?(project) @@ -70,6 +66,14 @@ module Gitlab def self.ci_pipeline_editor_page_enabled?(project) ::Feature.enabled?(:ci_pipeline_editor_page, project, default_enabled: false) end + + def self.allow_failure_with_exit_codes_enabled? + ::Feature.enabled?(:ci_allow_failure_with_exit_codes) + end + + def self.rules_variables_enabled?(project) + ::Feature.enabled?(:ci_rules_variables, project, default_enabled: false) + end end end end diff --git a/lib/gitlab/ci/limit.rb b/lib/gitlab/ci/limit.rb new file mode 100644 index 00000000000..c22a3c503d5 --- /dev/null +++ b/lib/gitlab/ci/limit.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + ## + # Abstract base class for CI/CD Quotas + # + class Limit + LimitExceededError = Class.new(StandardError) + + def initialize(_context, _resource) + end + + def enabled? + raise NotImplementedError + end + + def exceeded? + raise NotImplementedError + end + + def message + raise NotImplementedError + end + + def log_error!(extra_context = {}) + error = LimitExceededError.new(message) + # TODO: change this to Gitlab::ErrorTracking.log_exception(error, extra_context) + # https://gitlab.com/gitlab-org/gitlab/issues/32906 + ::Gitlab::ErrorTracking.track_exception(error, extra_context) + end + end + end +end diff --git a/lib/gitlab/ci/mask_secret.rb b/lib/gitlab/ci/mask_secret.rb index e5a7151b823..062ced9e234 100644 --- a/lib/gitlab/ci/mask_secret.rb +++ b/lib/gitlab/ci/mask_secret.rb @@ -9,11 +9,7 @@ module Gitlab # We assume 'value' must be mutable, given # that frozen string is enabled. - ## - # TODO We need to remove this because it is going to change checksum of - # a trace. - # - value.gsub!(token, 'x' * token.length) + value.gsub!(token, 'x' * token.bytesize) value end end diff --git a/lib/gitlab/ci/parsers.rb b/lib/gitlab/ci/parsers.rb index 0e44475607b..57f73c265b2 100644 --- a/lib/gitlab/ci/parsers.rb +++ b/lib/gitlab/ci/parsers.rb @@ -10,7 +10,8 @@ module Gitlab junit: ::Gitlab::Ci::Parsers::Test::Junit, cobertura: ::Gitlab::Ci::Parsers::Coverage::Cobertura, terraform: ::Gitlab::Ci::Parsers::Terraform::Tfplan, - accessibility: ::Gitlab::Ci::Parsers::Accessibility::Pa11y + accessibility: ::Gitlab::Ci::Parsers::Accessibility::Pa11y, + codequality: ::Gitlab::Ci::Parsers::Codequality::CodeClimate } end diff --git a/lib/gitlab/ci/parsers/codequality/code_climate.rb b/lib/gitlab/ci/parsers/codequality/code_climate.rb new file mode 100644 index 00000000000..628d50b84cb --- /dev/null +++ b/lib/gitlab/ci/parsers/codequality/code_climate.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Parsers + module Codequality + class CodeClimate + def parse!(json_data, codequality_report) + root = Gitlab::Json.parse(json_data) + + parse_all(root, codequality_report) + rescue JSON::ParserError => e + codequality_report.set_error_message("JSON parsing failed: #{e}") + end + + private + + def parse_all(root, codequality_report) + return unless root.present? + + root.each do |degradation| + break unless codequality_report.add_degradation(degradation) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/parsers/coverage/cobertura.rb b/lib/gitlab/ci/parsers/coverage/cobertura.rb index 934c797580c..1edcbac2f25 100644 --- a/lib/gitlab/ci/parsers/coverage/cobertura.rb +++ b/lib/gitlab/ci/parsers/coverage/cobertura.rb @@ -5,50 +5,113 @@ module Gitlab module Parsers module Coverage class Cobertura - CoberturaParserError = Class.new(Gitlab::Ci::Parsers::ParserError) + InvalidXMLError = Class.new(Gitlab::Ci::Parsers::ParserError) + InvalidLineInformationError = Class.new(Gitlab::Ci::Parsers::ParserError) - def parse!(xml_data, coverage_report) + GO_SOURCE_PATTERN = '/usr/local/go/src' + MAX_SOURCES = 100 + + def parse!(xml_data, coverage_report, project_path: nil, worktree_paths: nil) root = Hash.from_xml(xml_data) - parse_all(root, coverage_report) + context = { + project_path: project_path, + paths: worktree_paths&.to_set, + sources: [] + } + + parse_all(root, coverage_report, context) rescue Nokogiri::XML::SyntaxError - raise CoberturaParserError, "XML parsing failed" - rescue - raise CoberturaParserError, "Cobertura parsing failed" + raise InvalidXMLError, "XML parsing failed" end private - def parse_all(root, coverage_report) + def parse_all(root, coverage_report, context) return unless root.present? root.each do |key, value| - parse_node(key, value, coverage_report) + parse_node(key, value, coverage_report, context) end end - def parse_node(key, value, coverage_report) - return if key == 'sources' - - if key == 'class' + def parse_node(key, value, coverage_report, context) + if key == 'sources' && value['source'].present? + parse_sources(value['source'], context) + elsif key == 'package' Array.wrap(value).each do |item| - parse_class(item, coverage_report) + parse_package(item, coverage_report, context) + end + elsif key == 'class' + # This means the cobertura XML does not have classes within package nodes. + # This is possible in some cases like in simple JS project structures + # running Jest. + Array.wrap(value).each do |item| + parse_class(item, coverage_report, context) end elsif value.is_a?(Hash) - parse_all(value, coverage_report) + parse_all(value, coverage_report, context) elsif value.is_a?(Array) value.each do |item| - parse_all(item, coverage_report) + parse_all(item, coverage_report, context) end end end - def parse_class(file, coverage_report) + def parse_sources(sources, context) + return unless context[:project_path] && context[:paths] + + sources = Array.wrap(sources) + + # TODO: Go cobertura has a different format with how their packages + # are included in the filename. So we can't rely on the sources. + # We'll deal with this later. + return if sources.include?(GO_SOURCE_PATTERN) + + sources.each do |source| + source = build_source_path(source, context) + context[:sources] << source if source.present? + end + end + + def build_source_path(source, context) + # | raw source | extracted | + # |-----------------------------|------------| + # | /builds/foo/test/SampleLib/ | SampleLib/ | + # | /builds/foo/test/something | something | + # | /builds/foo/test/ | nil | + # | /builds/foo/test | nil | + source.split("#{context[:project_path]}/", 2)[1] + end + + def parse_package(package, coverage_report, context) + classes = package.dig('classes', 'class') + return unless classes.present? + + matched_filenames = Array.wrap(classes).map do |item| + parse_class(item, coverage_report, context) + end + + # Remove these filenames from the paths to avoid conflict + # with other packages that may contain the same class filenames + remove_matched_filenames(matched_filenames, context) + end + + def remove_matched_filenames(filenames, context) + return unless context[:paths] + + filenames.each { |f| context[:paths].delete(f) } + end + + def parse_class(file, coverage_report, context) return unless file["filename"].present? && file["lines"].present? parsed_lines = parse_lines(file["lines"]) + filename = determine_filename(file["filename"], context) + + coverage_report.add_file(filename, Hash[parsed_lines]) if filename - coverage_report.add_file(file["filename"], Hash[parsed_lines]) + filename end def parse_lines(lines) @@ -58,6 +121,27 @@ module Gitlab # Using `Integer()` here to raise exception on invalid values [Integer(line["number"]), Integer(line["hits"])] end + rescue + raise InvalidLineInformationError, "Line information had invalid values" + end + + def determine_filename(filename, context) + return filename unless context[:sources].any? + + full_filename = nil + + context[:sources].each_with_index do |source, index| + break if index >= MAX_SOURCES + break if full_filename = check_source(source, filename, context) + end + + full_filename + end + + def check_source(source, filename, context) + full_path = File.join(source, filename) + + return full_path if context[:paths].include?(full_path) end end end diff --git a/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb b/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb index a864c843dd8..2ca51930c19 100644 --- a/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb +++ b/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb @@ -35,7 +35,7 @@ module Gitlab # rubocop: enable CodeReuse/ActiveRecord def pipelines - if ::Feature.enabled?(:ci_auto_cancel_all_pipelines, project, default_enabled: false) + if ::Feature.enabled?(:ci_auto_cancel_all_pipelines, project, default_enabled: true) project.all_pipelines.ci_and_parent_sources else project.ci_pipelines diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index 06096a33f27..d05be54267c 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -12,7 +12,7 @@ module Gitlab :seeds_block, :variables_attributes, :push_options, :chat_data, :allow_mirror_update, :bridge, :content, :dry_run, # These attributes are set by Chains during processing: - :config_content, :yaml_processor_result, :stage_seeds + :config_content, :yaml_processor_result, :pipeline_seed ) do include Gitlab::Utils::StrongMemoize diff --git a/lib/gitlab/ci/pipeline/chain/limit/deployments.rb b/lib/gitlab/ci/pipeline/chain/limit/deployments.rb new file mode 100644 index 00000000000..d684eedcaac --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/limit/deployments.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Limit + class Deployments < Chain::Base + extend ::Gitlab::Utils::Override + include ::Gitlab::Ci::Pipeline::Chain::Helpers + + attr_reader :limit + private :limit + + def initialize(*) + super + + @limit = ::Gitlab::Ci::Pipeline::Quota::Deployments + .new(project.namespace, pipeline, command) + end + + override :perform! + def perform! + return unless limit.exceeded? + + limit.log_error!(project_id: project.id, plan: project.actual_plan_name) + error(limit.message, drop_reason: :deployments_limit_exceeded) + end + + override :break? + def break? + limit.exceeded? + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb index f9ae37aa273..654e24be8e1 100644 --- a/lib/gitlab/ci/pipeline/chain/populate.rb +++ b/lib/gitlab/ci/pipeline/chain/populate.rb @@ -10,12 +10,12 @@ module Gitlab PopulateError = Class.new(StandardError) def perform! - raise ArgumentError, 'missing stage seeds' unless @command.stage_seeds + raise ArgumentError, 'missing pipeline seed' unless @command.pipeline_seed ## # Populate pipeline with all stages, and stages with builds. # - pipeline.stages = @command.stage_seeds.map(&:to_resource) + pipeline.stages = @command.pipeline_seed.stages if stage_names.empty? return error('No stages / jobs for this pipeline.') diff --git a/lib/gitlab/ci/pipeline/chain/seed.rb b/lib/gitlab/ci/pipeline/chain/seed.rb index ba86b08d209..083f0bec1df 100644 --- a/lib/gitlab/ci/pipeline/chain/seed.rb +++ b/lib/gitlab/ci/pipeline/chain/seed.rb @@ -29,11 +29,11 @@ module Gitlab ## # Gather all runtime build/stage errors # - if stage_seeds_errors - return error(stage_seeds_errors.join("\n"), config_error: true) + if pipeline_seed.errors + return error(pipeline_seed.errors.join("\n"), config_error: true) end - @command.stage_seeds = stage_seeds + @command.pipeline_seed = pipeline_seed end def break? @@ -42,24 +42,12 @@ module Gitlab private - def stage_seeds_errors - stage_seeds.flat_map(&:errors).compact.presence - end - - def stage_seeds - strong_memoize(:stage_seeds) do - seeds = stages_attributes.inject([]) do |previous_stages, attributes| - seed = Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, attributes, previous_stages) - previous_stages + [seed] - end - - seeds.select(&:included?) + def pipeline_seed + strong_memoize(:pipeline_seed) do + stages_attributes = @command.yaml_processor_result.stages_attributes + Gitlab::Ci::Pipeline::Seed::Pipeline.new(pipeline, stages_attributes) end end - - def stages_attributes - @command.yaml_processor_result.stages_attributes - end end end end diff --git a/lib/gitlab/ci/pipeline/quota/deployments.rb b/lib/gitlab/ci/pipeline/quota/deployments.rb new file mode 100644 index 00000000000..ed32d0d3d49 --- /dev/null +++ b/lib/gitlab/ci/pipeline/quota/deployments.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Quota + class Deployments < ::Gitlab::Ci::Limit + include ::Gitlab::Utils::StrongMemoize + include ActionView::Helpers::TextHelper + + def initialize(namespace, pipeline, command) + @namespace = namespace + @pipeline = pipeline + @command = command + end + + def enabled? + limit > 0 + end + + def exceeded? + return false unless enabled? + + pipeline_deployment_count > limit + end + + def message + return unless exceeded? + + "Pipeline has too many deployments! Requested #{pipeline_deployment_count}, but the limit is #{limit}." + end + + private + + def pipeline_deployment_count + strong_memoize(:pipeline_deployment_count) do + @command.pipeline_seed.deployments_count + end + end + + def limit + strong_memoize(:limit) do + @namespace.actual_limits.ci_pipeline_deployments + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index 91dbcc616ea..2271915a72b 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -60,6 +60,7 @@ module Gitlab @seed_attributes .deep_merge(pipeline_attributes) .deep_merge(rules_attributes) + .deep_merge(allow_failure_criteria_attributes) .deep_merge(cache_attributes) end @@ -154,9 +155,15 @@ module Gitlab end def rules_attributes - return {} unless @using_rules + strong_memoize(:rules_attributes) do + next {} unless @using_rules - rules_result.build_attributes + if ::Gitlab::Ci::Features.rules_variables_enabled?(@pipeline.project) + rules_result.build_attributes(@seed_attributes) + else + rules_result.build_attributes + end + end end def rules_result @@ -176,6 +183,17 @@ module Gitlab @cache.build_attributes end end + + # If a job uses `allow_failure:exit_codes` and `rules:allow_failure` + # we need to prevent the exit codes from being persisted because they + # would break the behavior defined by `rules:allow_failure`. + def allow_failure_criteria_attributes + return {} unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled? + return {} if rules_attributes[:allow_failure].nil? + return {} unless @seed_attributes.dig(:options, :allow_failure_criteria) + + { options: { allow_failure_criteria: nil } } + end end end end diff --git a/lib/gitlab/ci/pipeline/seed/environment.rb b/lib/gitlab/ci/pipeline/seed/environment.rb index b20dc383419..5dff0788ec9 100644 --- a/lib/gitlab/ci/pipeline/seed/environment.rb +++ b/lib/gitlab/ci/pipeline/seed/environment.rb @@ -24,9 +24,7 @@ module Gitlab end def auto_stop_in - if Feature.enabled?(:environment_auto_stop_start_on_create) - job.environment_auto_stop_in - end + job.environment_auto_stop_in end def expanded_environment_name diff --git a/lib/gitlab/ci/pipeline/seed/pipeline.rb b/lib/gitlab/ci/pipeline/seed/pipeline.rb new file mode 100644 index 00000000000..da9d853cf68 --- /dev/null +++ b/lib/gitlab/ci/pipeline/seed/pipeline.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Seed + class Pipeline + include Gitlab::Utils::StrongMemoize + + def initialize(pipeline, stages_attributes) + @pipeline = pipeline + @stages_attributes = stages_attributes + end + + def errors + stage_seeds.flat_map(&:errors).compact.presence + end + + def stages + stage_seeds.map(&:to_resource) + end + + def size + stage_seeds.sum(&:size) + end + + def deployments_count + stage_seeds.sum do |stage_seed| + stage_seed.seeds.count do |build_seed| + build_seed.attributes[:environment].present? + end + end + end + + private + + def stage_seeds + strong_memoize(:stage_seeds) do + seeds = @stages_attributes.inject([]) do |previous_stages, attributes| + seed = Gitlab::Ci::Pipeline::Seed::Stage.new(@pipeline, attributes, previous_stages) + previous_stages + [seed] + end + + seeds.select(&:included?) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/accessibility_reports_comparer.rb b/lib/gitlab/ci/reports/accessibility_reports_comparer.rb index 210eb17f2d3..ab048672b22 100644 --- a/lib/gitlab/ci/reports/accessibility_reports_comparer.rb +++ b/lib/gitlab/ci/reports/accessibility_reports_comparer.rb @@ -3,52 +3,43 @@ module Gitlab module Ci module Reports - class AccessibilityReportsComparer - include Gitlab::Utils::StrongMemoize - - STATUS_SUCCESS = 'success' - STATUS_FAILED = 'failed' - - attr_reader :base_reports, :head_reports - - def initialize(base_reports, head_reports) - @base_reports = base_reports || AccessibilityReports.new - @head_reports = head_reports + class AccessibilityReportsComparer < ReportsComparer + def initialize(base_report, head_report) + @base_report = base_report || AccessibilityReports.new + @head_report = head_report end - def status - head_reports.errors_count > 0 ? STATUS_FAILED : STATUS_SUCCESS + def success? + head_report.errors_count == 0 end def existing_errors strong_memoize(:existing_errors) do - base_reports.all_errors + base_report.all_errors & head_report.all_errors end end def new_errors strong_memoize(:new_errors) do - head_reports.all_errors - base_reports.all_errors + head_report.all_errors - base_report.all_errors end end def resolved_errors strong_memoize(:resolved_errors) do - base_reports.all_errors - head_reports.all_errors + base_report.all_errors - head_report.all_errors end end - def errors_count - head_reports.errors_count - end - def resolved_count resolved_errors.size end def total_count - existing_errors.size + new_errors.size + head_report.errors_count end + + alias_method :errors_count, :total_count end end end diff --git a/lib/gitlab/ci/reports/codequality_reports.rb b/lib/gitlab/ci/reports/codequality_reports.rb new file mode 100644 index 00000000000..060a1e2399b --- /dev/null +++ b/lib/gitlab/ci/reports/codequality_reports.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + class CodequalityReports + attr_reader :degradations, :error_message + + CODECLIMATE_SCHEMA_PATH = Rails.root.join('app', 'validators', 'json_schemas', 'codeclimate.json').to_s + + def initialize + @degradations = {}.with_indifferent_access + @error_message = nil + end + + def add_degradation(degradation) + valid_degradation?(degradation) && @degradations[degradation.dig('fingerprint')] = degradation + end + + def set_error_message(error) + @error_message = error + end + + def degradations_count + @degradations.size + end + + def all_degradations + @degradations.values + end + + private + + def valid_degradation?(degradation) + JSON::Validator.validate!(CODECLIMATE_SCHEMA_PATH, degradation) + rescue JSON::Schema::ValidationError => e + set_error_message("Invalid degradation format: #{e.message}") + false + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/codequality_reports_comparer.rb b/lib/gitlab/ci/reports/codequality_reports_comparer.rb new file mode 100644 index 00000000000..88e02cd9004 --- /dev/null +++ b/lib/gitlab/ci/reports/codequality_reports_comparer.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + class CodequalityReportsComparer < ReportsComparer + def initialize(base_report, head_report) + @base_report = base_report || CodequalityReports.new + @head_report = head_report + end + + def success? + head_report.degradations_count == 0 + end + + def existing_errors + strong_memoize(:existing_errors) do + base_report.all_degradations & head_report.all_degradations + end + end + + def new_errors + strong_memoize(:new_errors) do + fingerprints = head_report.degradations.keys - base_report.degradations.keys + head_report.degradations.fetch_values(*fingerprints) + end + end + + def resolved_errors + strong_memoize(:resolved_errors) do + fingerprints = base_report.degradations.keys - head_report.degradations.keys + base_report.degradations.fetch_values(*fingerprints) + end + end + + def resolved_count + resolved_errors.size + end + + def total_count + head_report.degradations_count + end + + alias_method :errors_count, :total_count + end + end + end +end diff --git a/lib/gitlab/ci/reports/reports_comparer.rb b/lib/gitlab/ci/reports/reports_comparer.rb new file mode 100644 index 00000000000..d413d3a74f6 --- /dev/null +++ b/lib/gitlab/ci/reports/reports_comparer.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + class ReportsComparer + include Gitlab::Utils::StrongMemoize + + STATUS_SUCCESS = 'success' + STATUS_FAILED = 'failed' + + attr_reader :base_report, :head_report + + def initialize(base_report, head_report) + @base_report = base_report + @head_report = head_report + end + + def status + success? ? STATUS_SUCCESS : STATUS_FAILED + end + + def success? + raise NotImplementedError + end + + def existing_errors + raise NotImplementedError + end + + def new_errors + raise NotImplementedError + end + + def resolved_errors + raise NotImplementedError + end + + def errors_count + raise NotImplementedError + end + + def resolved_count + resolved_errors.size + end + + def total_count + existing_errors.size + new_errors.size + end + end + end + end +end diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml index fe23641802b..2ae9730ec1a 100644 --- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml @@ -7,7 +7,7 @@ code_quality: variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" - CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18" + CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18-gitlab.1" needs: [] script: - export SOURCE_CODE=$PWD diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml index 385959389de..e5b40e5f49a 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml @@ -1,5 +1,5 @@ .auto-deploy: - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v2.0.0-beta.2" + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v2.0.0" dependencies: [] review: diff --git a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml index 3b87d53f165..895e6e8ea6d 100644 --- a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml @@ -2,6 +2,8 @@ test: variables: POSTGRES_VERSION: 9.6.16 POSTGRES_DB: test + POSTGRES_USER: user + POSTGRES_PASSWORD: testing-password services: - "postgres:${POSTGRES_VERSION}" stage: test diff --git a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml index 3f62d92ad13..23dfeda31cc 100644 --- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml @@ -1,6 +1,6 @@ apply: stage: deploy - image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.34.1" + image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.36.0" environment: name: production variables: diff --git a/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml b/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml index 65abee1f5eb..3faf07546de 100644 --- a/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: ayufan/openshift-cli +image: openshift/origin-cli stages: - build # dummy stage to follow the template guidelines diff --git a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml index 0ae8fd833c4..135f0df99fe 100644 --- a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml @@ -15,7 +15,8 @@ variables: FUZZAPI_VERSION: latest FUZZAPI_CONFIG: .gitlab-api-fuzzing.yml FUZZAPI_TIMEOUT: 30 - FUZZAPI_REPORT: gl-api-fuzzing-report.xml + FUZZAPI_REPORT: gl-api-fuzzing-report.json + FUZZAPI_REPORT_ASSET_PATH: assets # FUZZAPI_D_NETWORK: testing-net # @@ -45,6 +46,7 @@ apifuzzer_fuzz: variables: FUZZAPI_PROJECT: $CI_PROJECT_PATH FUZZAPI_API: http://apifuzzer:80 + FUZZAPI_NEW_REPORT: 1 TZ: America/Los_Angeles services: - name: $FUZZAPI_IMAGE @@ -61,7 +63,7 @@ apifuzzer_fuzz: - if: $API_FUZZING_DISABLED_FOR_DEFAULT_BRANCH && $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME when: never - - if: $GITLAB_FEATURES =~ /\bapi_fuzzing\b/ + - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bapi_fuzzing\b/ script: # # Validate options @@ -75,6 +77,9 @@ apifuzzer_fuzz: # Run user provided pre-script - sh -c "$FUZZAPI_PRE_SCRIPT" # + # Make sure asset path exists + - mkdir -p $FUZZAPI_REPORT_ASSET_PATH + # # Start scanning - worker-entry # @@ -82,8 +87,12 @@ apifuzzer_fuzz: - sh -c "$FUZZAPI_POST_SCRIPT" # artifacts: + when: always + paths: + - $FUZZAPI_REPORT_ASSET_PATH + - $FUZZAPI_REPORT reports: - junit: $FUZZAPI_REPORT + api_fuzzing: $FUZZAPI_REPORT apifuzzer_fuzz_dnd: stage: fuzz @@ -102,7 +111,7 @@ apifuzzer_fuzz_dnd: - if: $API_FUZZING_DISABLED_FOR_DEFAULT_BRANCH && $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME when: never - - if: $GITLAB_FEATURES =~ /\bapi_fuzzing\b/ + - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bapi_fuzzing\b/ services: - docker:19.03.12-dind script: @@ -115,6 +124,9 @@ apifuzzer_fuzz_dnd: # Run user provided pre-script - sh -c "$FUZZAPI_PRE_SCRIPT" # + # Make sure asset path exists + - mkdir -p $FUZZAPI_REPORT_ASSET_PATH + # # Start peach testing engine container - | docker run -d \ @@ -155,6 +167,8 @@ apifuzzer_fuzz_dnd: -e FUZZAPI_PROFILE \ -e FUZZAPI_CONFIG \ -e FUZZAPI_REPORT \ + -e FUZZAPI_REPORT_ASSET_PATH \ + -e FUZZAPI_NEW_REPORT=1 \ -e FUZZAPI_HAR \ -e FUZZAPI_OPENAPI \ -e FUZZAPI_POSTMAN_COLLECTION \ @@ -168,6 +182,8 @@ apifuzzer_fuzz_dnd: -e FUZZAPI_SERVICE_START_TIMEOUT \ -e FUZZAPI_HTTP_USERNAME \ -e FUZZAPI_HTTP_PASSWORD \ + -e CI_PROJECT_URL \ + -e CI_JOB_ID \ -e CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH} \ $FUZZAPI_D_WORKER_ENV \ $FUZZAPI_D_WORKER_PORTS \ @@ -193,6 +209,8 @@ apifuzzer_fuzz_dnd: -e FUZZAPI_PROFILE \ -e FUZZAPI_CONFIG \ -e FUZZAPI_REPORT \ + -e FUZZAPI_REPORT_ASSET_PATH \ + -e FUZZAPI_NEW_REPORT=1 \ -e FUZZAPI_HAR \ -e FUZZAPI_OPENAPI \ -e FUZZAPI_POSTMAN_COLLECTION \ @@ -206,7 +224,10 @@ apifuzzer_fuzz_dnd: -e FUZZAPI_SERVICE_START_TIMEOUT \ -e FUZZAPI_HTTP_USERNAME \ -e FUZZAPI_HTTP_PASSWORD \ + -e CI_PROJECT_URL \ + -e CI_JOB_ID \ -v $CI_PROJECT_DIR:/app \ + -v `pwd`/$FUZZAPI_REPORT_ASSET_PATH:/app/$FUZZAPI_REPORT_ASSET_PATH:rw \ -p 81:80 \ -p 8001:8000 \ -p 515:514 \ @@ -239,7 +260,9 @@ apifuzzer_fuzz_dnd: paths: - ./gl-api_fuzzing*.log - ./gl-api_fuzzing*.zip + - $FUZZAPI_REPORT_ASSET_PATH + - $FUZZAPI_REPORT reports: - junit: $FUZZAPI_REPORT + api_fuzzing: $FUZZAPI_REPORT # end diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml index 3cbde9d30c8..5ea2363a0c5 100644 --- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml @@ -8,7 +8,7 @@ variables: container_scanning: stage: test - image: $SECURE_ANALYZERS_PREFIX/klar:$CS_MAJOR_VERSION + image: "$CS_ANALYZER_IMAGE" variables: # By default, use the latest clair vulnerabilities database, however, allow it to be overridden here with a specific image # to enable container scanning to run offline, or to provide a consistent list of vulnerabilities for integration testing purposes @@ -18,6 +18,7 @@ container_scanning: # file. See https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html#overriding-the-container-scanning-template # for details GIT_STRATEGY: none + CS_ANALYZER_IMAGE: $SECURE_ANALYZERS_PREFIX/klar:$CS_MAJOR_VERSION allow_failure: true services: - name: $CLAIR_DB_IMAGE diff --git a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml index a1b6dc2cc1b..9d47537c0f0 100644 --- a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml @@ -12,7 +12,7 @@ variables: coverage_fuzzing_unlicensed: - stage: test + stage: .pre allow_failure: true rules: - if: $GITLAB_FEATURES !~ /\bcoverage_fuzzing\b/ && $COVFUZZ_DISABLED == null diff --git a/lib/gitlab/ci/templates/Security/DAST-On-Demand-Scan.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST-On-Demand-Scan.gitlab-ci.yml new file mode 100644 index 00000000000..a0564a16c07 --- /dev/null +++ b/lib/gitlab/ci/templates/Security/DAST-On-Demand-Scan.gitlab-ci.yml @@ -0,0 +1,24 @@ +stages: + - build + - test + - deploy + - dast + +variables: + DAST_VERSION: 1 + # Setting this variable will affect all Security templates + # (SAST, Dependency Scanning, ...) + SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers" + +dast: + stage: dast + image: + name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION" + variables: + GIT_STRATEGY: none + allow_failure: true + script: + - /analyze + artifacts: + reports: + dast: gl-dast-report.json diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml index 3789f0edc1c..b534dad9593 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -28,11 +28,8 @@ dependency_scanning: .ds-analyzer: extends: dependency_scanning allow_failure: true - rules: - - if: $DEPENDENCY_SCANNING_DISABLED - when: never - - if: $CI_COMMIT_BRANCH && - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ + # `rules` must be overridden explicitly by each child job + # see https://gitlab.com/gitlab-org/gitlab/-/issues/218444 script: - /analyzer run diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml index a51cb61da6d..f4ee8ebd47e 100644 --- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -30,10 +30,8 @@ sast: .sast-analyzer: extends: sast allow_failure: true - rules: - - if: $SAST_DISABLED - when: never - - if: $CI_COMMIT_BRANCH + # `rules` must be overridden explicitly by each child job + # see https://gitlab.com/gitlab-org/gitlab/-/issues/218444 script: - /analyzer run @@ -175,7 +173,7 @@ nodejs-scan-sast: - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /nodejs-scan/ exists: - - 'package.json' + - '**/package.json' phpcs-security-audit-sast: extends: .sast-analyzer diff --git a/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml index 6ebff102ccb..8ca1d2e08ba 100644 --- a/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml @@ -14,6 +14,9 @@ variables: stage: test image: "$SECURE_ANALYZERS_PREFIX/secrets:$SECRETS_ANALYZER_VERSION" services: [] + allow_failure: true + # `rules` must be overridden explicitly by each child job + # see https://gitlab.com/gitlab-org/gitlab/-/issues/218444 artifacts: reports: secret_detection: gl-secret-detection-report.json diff --git a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml index e455bfac9de..910e711f046 100644 --- a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml @@ -56,5 +56,6 @@ cache: .destroy: &destroy stage: cleanup script: + - cd ${TF_ROOT} - gitlab-terraform destroy when: manual diff --git a/lib/gitlab/ci/templates/npm.gitlab-ci.yml b/lib/gitlab/ci/templates/npm.gitlab-ci.yml index 0a739cf122d..035ba52da84 100644 --- a/lib/gitlab/ci/templates/npm.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/npm.gitlab-ci.yml @@ -55,5 +55,5 @@ publish_package: npm publish && echo "Successfully published version ${NPM_PACKAGE_VERSION} of ${NPM_PACKAGE_NAME} to GitLab's NPM registry: ${CI_PROJECT_URL}/-/packages" } || { - echo "No new version of ${NPM_PACKAGE_NAME} published. This is most likely because version ${NPM_PACKAGE_VERSION} already exists in GitLab's NPM registry."; exit 1 + echo "No new version of ${NPM_PACKAGE_NAME} published. This is most likely because version ${NPM_PACKAGE_VERSION} already exists in GitLab's NPM registry." } diff --git a/lib/gitlab/ci/templates/npm.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/npm.latest.gitlab-ci.yml new file mode 100644 index 00000000000..536cf9bd8d8 --- /dev/null +++ b/lib/gitlab/ci/templates/npm.latest.gitlab-ci.yml @@ -0,0 +1,41 @@ +publish: + image: node:latest + stage: deploy + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_REF_NAME =~ /^v\d+\.\d+\.\d+.*$/ + changes: + - package.json + script: + # If no .npmrc if included in the repo, generate a temporary one that is configured to publish to GitLab's NPM registry + - | + if [[ ! -f .npmrc ]]; then + echo 'No .npmrc found! Creating one now. Please review the following link for more information: https://docs.gitlab.com/ee/user/packages/npm_registry/index.html#project-level-npm-endpoint-1' + { + echo "@${CI_PROJECT_ROOT_NAMESPACE}:registry=${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/" + echo "${CI_API_V4_URL#http*:}/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=\${CI_JOB_TOKEN}" + } >> .npmrc + fi + - echo "Created the following .npmrc:"; cat .npmrc + + # Extract a few values from package.json + - NPM_PACKAGE_NAME=$(node -p "require('./package.json').name") + - NPM_PACKAGE_VERSION=$(node -p "require('./package.json').version") + + # Validate that the package name is properly scoped to the project's root namespace. + # For more information, see https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention + - | + if [[ ! $NPM_PACKAGE_NAME =~ ^@$CI_PROJECT_ROOT_NAMESPACE/ ]]; then + echo "Invalid package scope! Packages must be scoped in the root namespace of the project, e.g. \"@${CI_PROJECT_ROOT_NAMESPACE}/${CI_PROJECT_NAME}\"" + echo 'For more information, see https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention' + exit 1 + fi + + # Compare the version in package.json to all published versions. + # If the package.json version has not yet been published, run `npm publish`. + - | + if [[ $(npm view "${NPM_PACKAGE_NAME}" versions) != *"'${NPM_PACKAGE_VERSION}'"* ]]; then + npm publish + echo "Successfully published version ${NPM_PACKAGE_VERSION} of ${NPM_PACKAGE_NAME} to GitLab's NPM registry: ${CI_PROJECT_URL}/-/packages" + else + echo "Version ${NPM_PACKAGE_VERSION} of ${NPM_PACKAGE_NAME} has already been published, so no new version has been published." + fi diff --git a/lib/gitlab/ci/trace/checksum.rb b/lib/gitlab/ci/trace/checksum.rb index 62532ef1cd2..7cdb6a6c03c 100644 --- a/lib/gitlab/ci/trace/checksum.rb +++ b/lib/gitlab/ci/trace/checksum.rb @@ -64,10 +64,33 @@ module Gitlab end end + def state_bytesize + strong_memoize(:state_bytesize) do + build.pending_state&.trace_bytesize + end + end + + def trace_size + strong_memoize(:trace_size) do + trace_chunks.sum { |chunk| chunk_size(chunk) } + end + end + + def corrupted? + return false unless has_bytesize? + return false if valid? + + state_bytesize.to_i != trace_size.to_i + end + def chunks_count trace_chunks.to_a.size end + def has_bytesize? + state_bytesize.present? + end + private def chunk_size(chunk) diff --git a/lib/gitlab/ci/trace/metrics.rb b/lib/gitlab/ci/trace/metrics.rb index 097436d84ea..ce9efbda7ea 100644 --- a/lib/gitlab/ci/trace/metrics.rb +++ b/lib/gitlab/ci/trace/metrics.rb @@ -18,7 +18,8 @@ module Gitlab :conflict, # runner has sent unrecognized build state details :locked, # build trace has been locked by a different mechanism :stalled, # failed to migrate chunk due to a worker duplication - :invalid # malformed build trace has been detected using CRC32 + :invalid, # invalid build trace has been detected using CRC32 + :corrupted # malformed trace found after comparing CRC32 and size ].freeze def increment_trace_operation(operation: :unknown) diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb index 52a00e41214..cd7d781a574 100644 --- a/lib/gitlab/ci/yaml_processor/result.rb +++ b/lib/gitlab/ci/yaml_processor/result.rb @@ -77,6 +77,7 @@ module Gitlab options: { image: job[:image], services: job[:services], + allow_failure_criteria: job[:allow_failure_criteria], artifacts: job[:artifacts], dependencies: job[:dependencies], cross_dependencies: job.dig(:needs, :cross_dependency), diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb index 2a386657e0b..88786ed82ff 100644 --- a/lib/gitlab/config/entry/validators.rb +++ b/lib/gitlab/config/entry/validators.rb @@ -134,6 +134,16 @@ module Gitlab end end + class HashOrBooleanValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless value.is_a?(Hash) || validate_boolean(value) + record.errors.add(attribute, 'should be a hash or a boolean value') + end + end + end + class KeyValidator < ActiveModel::EachValidator include LegacyValidationHelpers @@ -158,6 +168,22 @@ module Gitlab end end + class ArrayOfIntegersOrIntegerValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_integer(value) || validate_array_of_integers(value) + record.errors.add(attribute, 'should be an array of integers or an integer') + end + end + + private + + def validate_array_of_integers(values) + values.is_a?(Array) && values.all? { |value| validate_integer(value) } + end + end + class RegexpValidator < ActiveModel::EachValidator include LegacyValidationHelpers diff --git a/lib/gitlab/cycle_analytics/builds_event_helper.rb b/lib/gitlab/cycle_analytics/builds_event_helper.rb index 0d6f32fdc6f..c39d41578e9 100644 --- a/lib/gitlab/cycle_analytics/builds_event_helper.rb +++ b/lib/gitlab/cycle_analytics/builds_event_helper.rb @@ -3,11 +3,11 @@ module Gitlab module CycleAnalytics module BuildsEventHelper - def initialize(*args) + def initialize(...) @projections = [build_table[:id]] @order = build_table[:created_at] - super(*args) + super(...) end def fetch diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb index d75da76415a..790bf32c6c7 100644 --- a/lib/gitlab/cycle_analytics/code_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb @@ -5,7 +5,7 @@ module Gitlab class CodeEventFetcher < BaseEventFetcher include CodeHelper - def initialize(*args) + def initialize(...) @projections = [mr_table[:title], mr_table[:iid], mr_table[:id], @@ -14,7 +14,7 @@ module Gitlab mr_table[:author_id]] @order = mr_table[:created_at] - super(*args) + super(...) end private diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb index 6914cf24c19..fd04ec090b3 100644 --- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb @@ -5,14 +5,14 @@ module Gitlab class IssueEventFetcher < BaseEventFetcher include IssueHelper - def initialize(*args) + def initialize(...) @projections = [issue_table[:title], issue_table[:iid], issue_table[:id], issue_table[:created_at], issue_table[:author_id]] - super(*args) + super(...) end private diff --git a/lib/gitlab/cycle_analytics/permissions.rb b/lib/gitlab/cycle_analytics/permissions.rb index 55214e6b896..0e094fabb01 100644 --- a/lib/gitlab/cycle_analytics/permissions.rb +++ b/lib/gitlab/cycle_analytics/permissions.rb @@ -12,8 +12,8 @@ module Gitlab production: :read_issue }.freeze - def self.get(*args) - new(*args).get + def self.get(...) + new(...).get end def initialize(user:, project:) diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb index bad02e00a13..4d98d589e46 100644 --- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb @@ -5,14 +5,14 @@ module Gitlab class PlanEventFetcher < BaseEventFetcher include PlanHelper - def initialize(*args) + def initialize(...) @projections = [issue_table[:title], issue_table[:iid], issue_table[:id], issue_table[:created_at], issue_table[:author_id]] - super(*args) + super(...) end private diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb index 8843ab2bcb9..5fa286bd3df 100644 --- a/lib/gitlab/cycle_analytics/production_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb @@ -5,7 +5,7 @@ module Gitlab class ProductionEventFetcher < BaseEventFetcher include ProductionHelper - def initialize(*args) + def initialize(...) @projections = [issue_table[:title], issue_table[:iid], issue_table[:id], @@ -13,7 +13,7 @@ module Gitlab issue_table[:author_id], routes_table[:path]] - super(*args) + super(...) end private diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb index f5f8c19683d..0b7d160c7de 100644 --- a/lib/gitlab/cycle_analytics/review_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb @@ -5,7 +5,7 @@ module Gitlab class ReviewEventFetcher < BaseEventFetcher include ReviewHelper - def initialize(*args) + def initialize(...) @projections = [mr_table[:title], mr_table[:iid], mr_table[:id], @@ -13,7 +13,7 @@ module Gitlab mr_table[:state_id], mr_table[:author_id]] - super(*args) + super(...) end private diff --git a/lib/gitlab/cycle_analytics/updater.rb b/lib/gitlab/cycle_analytics/updater.rb index c642809a792..5be351989e0 100644 --- a/lib/gitlab/cycle_analytics/updater.rb +++ b/lib/gitlab/cycle_analytics/updater.rb @@ -3,8 +3,8 @@ module Gitlab module CycleAnalytics class Updater - def self.update!(*args) - new(*args).update! + def self.update!(...) + new(...).update! end def initialize(event_result, from:, to:, klass:) diff --git a/lib/gitlab/cycle_analytics/usage_data.rb b/lib/gitlab/cycle_analytics/usage_data.rb deleted file mode 100644 index e58def57e69..00000000000 --- a/lib/gitlab/cycle_analytics/usage_data.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module CycleAnalytics - class UsageData - include Gitlab::Utils::StrongMemoize - PROJECTS_LIMIT = 10 - - attr_reader :options - - def initialize - @options = { from: 7.days.ago } - end - - def projects - strong_memoize(:projects) do - projects = Project.where.not(last_activity_at: nil).order(last_activity_at: :desc).limit(10) + - Project.where.not(last_repository_updated_at: nil).order(last_repository_updated_at: :desc).limit(10) - - projects = projects.uniq.sort_by do |project| - [project.last_activity_at, project.last_repository_updated_at].min - end - - if projects.size < 10 - projects.concat(Project.where(last_activity_at: nil, last_repository_updated_at: nil).limit(10)) - end - - projects.uniq.first(10) - end - end - - def to_json(*) - total = 0 - - values = - medians_per_stage.each_with_object({}) do |(stage_name, medians), hsh| - calculations = stage_values(medians) - - total += calculations.values.compact.sum - hsh[stage_name] = calculations - end - - values[:total] = total - - { avg_cycle_analytics: values } - end - - private - - def medians_per_stage - projects.each_with_object({}) do |project, hsh| - ::CycleAnalytics::ProjectLevel.new(project, options: options).all_medians_by_stage.each do |stage_name, median| - hsh[stage_name] ||= [] - hsh[stage_name] << median - end - end - end - - def stage_values(medians) - medians = medians.map(&:presence).compact - average = calc_average(medians) - - { - average: average, - sd: standard_deviation(medians, average), - missing: projects.length - medians.length - } - end - - def calc_average(values) - return if values.empty? - - (values.sum / values.length).to_i - end - - def standard_deviation(values, average) - Math.sqrt(sample_variance(values, average)).to_i - end - - def sample_variance(values, average) - return 0 if values.length <= 1 - - sum = values.inject(0) do |acc, val| - acc + (val - average)**2 - end - - sum / (values.length - 1) - end - end - end -end diff --git a/lib/gitlab/danger/base_linter.rb b/lib/gitlab/danger/base_linter.rb new file mode 100644 index 00000000000..df2e9e745aa --- /dev/null +++ b/lib/gitlab/danger/base_linter.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Gitlab + module Danger + class BaseLinter + MIN_SUBJECT_WORDS_COUNT = 3 + MAX_LINE_LENGTH = 72 + WIP_PREFIX = 'WIP: ' + + attr_reader :commit, :problems + + def self.problems_mapping + { + subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words", + subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters", + subject_starts_with_lowercase: "The %s must start with a capital letter", + subject_ends_with_a_period: "The %s must not end with a period" + } + end + + def self.subject_description + 'commit subject' + end + + def initialize(commit) + @commit = commit + @problems = {} + end + + def failed? + problems.any? + end + + def add_problem(problem_key, *args) + @problems[problem_key] = sprintf(self.class.problems_mapping[problem_key], *args) + end + + def lint_subject + if subject_too_short? + add_problem(:subject_too_short, self.class.subject_description) + end + + if subject_too_long? + add_problem(:subject_too_long, self.class.subject_description) + end + + if subject_starts_with_lowercase? + add_problem(:subject_starts_with_lowercase, self.class.subject_description) + end + + if subject_ends_with_a_period? + add_problem(:subject_ends_with_a_period, self.class.subject_description) + end + + self + end + + private + + def subject + message_parts[0].delete_prefix(WIP_PREFIX) + end + + def subject_too_short? + subject.split(' ').length < MIN_SUBJECT_WORDS_COUNT + end + + def subject_too_long? + line_too_long?(subject) + end + + def line_too_long?(line) + line.length > MAX_LINE_LENGTH + end + + def subject_starts_with_lowercase? + return false if ('A'..'Z').cover?(subject[0]) + + first_char = subject.sub(/\A(\[.+\]|\w+:)\s/, '')[0] + first_char_downcased = first_char.downcase + return true unless ('a'..'z').cover?(first_char_downcased) + + first_char.downcase == first_char + end + + def subject_ends_with_a_period? + subject.end_with?('.') + end + + def message_parts + @message_parts ||= commit.message.split("\n", 3) + end + end + end +end diff --git a/lib/gitlab/danger/changelog.rb b/lib/gitlab/danger/changelog.rb index 607ca1200a0..92af6849b2f 100644 --- a/lib/gitlab/danger/changelog.rb +++ b/lib/gitlab/danger/changelog.rb @@ -39,6 +39,7 @@ module Gitlab def required? git.added_files.any? { |path| path =~ %r{\Adb/(migrate|post_migrate)/} } end + alias_method :db_changes?, :required? def optional? categories_need_changelog? && without_no_changelog_label? diff --git a/lib/gitlab/danger/commit_linter.rb b/lib/gitlab/danger/commit_linter.rb index 2e469359bdc..7e2e0fb0acb 100644 --- a/lib/gitlab/danger/commit_linter.rb +++ b/lib/gitlab/danger/commit_linter.rb @@ -1,40 +1,37 @@ # frozen_string_literal: true +require_relative 'base_linter' + emoji_checker_path = File.expand_path('emoji_checker', __dir__) defined?(Rails) ? require_dependency(emoji_checker_path) : require_relative(emoji_checker_path) module Gitlab module Danger - class CommitLinter - MIN_SUBJECT_WORDS_COUNT = 3 - MAX_LINE_LENGTH = 72 + class CommitLinter < BaseLinter MAX_CHANGED_FILES_IN_COMMIT = 3 MAX_CHANGED_LINES_IN_COMMIT = 30 SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(?<!`)(#|!|&|%)\d+(?<!`)}.freeze - DEFAULT_SUBJECT_DESCRIPTION = 'commit subject' - WIP_PREFIX = 'WIP: ' - PROBLEMS = { - subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words", - subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters", - subject_starts_with_lowercase: "The %s must start with a capital letter", - subject_ends_with_a_period: "The %s must not end with a period", - separator_missing: "The commit subject and body must be separated by a blank line", - details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \ + + def self.problems_mapping + super.merge( + { + separator_missing: "The commit subject and body must be separated by a blank line", + details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \ "at least #{MAX_CHANGED_FILES_IN_COMMIT} files must describe these changes in the commit body", - details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line", - message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \ + details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line", + message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \ "to the commit message, and are displayed as plain text outside of GitLab", - message_contains_unicode_emoji: "Avoid the use of Unicode Emoji. These add no value to the commit " \ + message_contains_unicode_emoji: "Avoid the use of Unicode Emoji. These add no value to the commit " \ "message, and may not be displayed properly everywhere", - message_contains_short_reference: "Use full URLs instead of short references (`gitlab-org/gitlab#123` or " \ + message_contains_short_reference: "Use full URLs instead of short references (`gitlab-org/gitlab#123` or " \ "`!123`), as short references are displayed as plain text outside of GitLab" - }.freeze - - attr_reader :commit, :problems + } + ) + end def initialize(commit) - @commit = commit - @problems = {} + super + @linted = false end @@ -58,19 +55,11 @@ module Gitlab !details.nil? && !details.empty? end - def failed? - problems.any? - end - - def add_problem(problem_key, *args) - @problems[problem_key] = sprintf(PROBLEMS[problem_key], *args) - end - - def lint(subject_description = "commit subject") + def lint return self if @linted @linted = true - lint_subject(subject_description) + lint_subject lint_separator lint_details lint_message @@ -78,26 +67,6 @@ module Gitlab self end - def lint_subject(subject_description) - if subject_too_short? - add_problem(:subject_too_short, subject_description) - end - - if subject_too_long? - add_problem(:subject_too_long, subject_description) - end - - if subject_starts_with_lowercase? - add_problem(:subject_starts_with_lowercase, subject_description) - end - - if subject_ends_with_a_period? - add_problem(:subject_ends_with_a_period, subject_description) - end - - self - end - private def lint_separator @@ -114,15 +83,11 @@ module Gitlab end details&.each_line do |line| - line = line.strip - - next unless line_too_long?(line) - - url_size = line.scan(%r((https?://\S+))).sum { |(url)| url.length } + line_without_urls = line.strip.gsub(%r{https?://\S+}, '') # If the line includes a URL, we'll allow it to exceed MAX_LINE_LENGTH characters, but # only if the line _without_ the URL does not exceed this limit. - next unless line_too_long?(line.length - url_size) + next unless line_too_long?(line_without_urls) add_problem(:details_line_too_long) break @@ -159,10 +124,6 @@ module Gitlab files_changed > MAX_CHANGED_FILES_IN_COMMIT && lines_changed > MAX_CHANGED_LINES_IN_COMMIT end - def subject - message_parts[0].delete_prefix(WIP_PREFIX) - end - def separator message_parts[1] end @@ -171,37 +132,6 @@ module Gitlab message_parts[2]&.gsub(/^Signed-off-by.*$/, '') end - def line_too_long?(line) - case line - when String - line.length > MAX_LINE_LENGTH - when Integer - line > MAX_LINE_LENGTH - else - raise ArgumentError, "The line argument (#{line}) should be a String or an Integer! #{line.class} given." - end - end - - def subject_too_short? - subject.split(' ').length < MIN_SUBJECT_WORDS_COUNT - end - - def subject_too_long? - line_too_long?(subject) - end - - def subject_starts_with_lowercase? - first_char = subject.sub(/\A(\[.+\]|\w+:)\s/, '')[0] - first_char_downcased = first_char.downcase - return true unless ('a'..'z').cover?(first_char_downcased) - - first_char.downcase == first_char - end - - def subject_ends_with_a_period? - subject.end_with?('.') - end - def message_contains_text_emoji? emoji_checker.includes_text_emoji?(commit.message) end @@ -217,10 +147,6 @@ module Gitlab def emoji_checker @emoji_checker ||= Gitlab::Danger::EmojiChecker.new end - - def message_parts - @message_parts ||= commit.message.split("\n", 3) - end end end end diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb index 89f21e8bd23..d22f28ff7f2 100644 --- a/lib/gitlab/danger/helper.rb +++ b/lib/gitlab/danger/helper.rb @@ -64,7 +64,7 @@ module Gitlab # - respond_to?(:gitlab) # - respond_to?(:gitlab, true) gitlab - rescue NoMethodError + rescue NameError nil end @@ -268,6 +268,10 @@ module Gitlab def has_database_scoped_labels?(current_mr_labels) current_mr_labels.any? { |label| label.start_with?('database::') } end + + def has_ci_changes? + changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any? + end end end end diff --git a/lib/gitlab/danger/merge_request_linter.rb b/lib/gitlab/danger/merge_request_linter.rb new file mode 100644 index 00000000000..d401d332aa7 --- /dev/null +++ b/lib/gitlab/danger/merge_request_linter.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative 'base_linter' + +module Gitlab + module Danger + class MergeRequestLinter < BaseLinter + alias_method :lint, :lint_subject + + def self.subject_description + 'merge request title' + end + + def self.mr_run_options_regex + [ + 'RUN AS-IF-FOSS', + 'UPDATE CACHE', + 'RUN ALL RSPEC', + 'SKIP RSPEC FAIL-FAST' + ].join('|') + end + + private + + def subject + super.gsub(/\[?(#{self.class.mr_run_options_regex})\]?/, '').strip + end + end + end +end diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb index 23f877b4e0f..328083f7002 100644 --- a/lib/gitlab/danger/roulette.rb +++ b/lib/gitlab/danger/roulette.rb @@ -24,7 +24,7 @@ module Gitlab # # @return [Array<Spin>] def spin(project, categories, timezone_experiment: false) - spins = categories.map do |category| + spins = categories.sort.map do |category| including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(category, timezone_experiment) spin_for_category(project, category, timezone_experiment: including_timezone) diff --git a/lib/gitlab/database/batch_count.rb b/lib/gitlab/database/batch_count.rb index 6f79e965cd5..5a506da0d05 100644 --- a/lib/gitlab/database/batch_count.rb +++ b/lib/gitlab/database/batch_count.rb @@ -49,6 +49,8 @@ module Gitlab MAX_ALLOWED_LOOPS = 10_000 SLEEP_TIME_IN_SECONDS = 0.01 # 10 msec sleep ALLOWED_MODES = [:itself, :distinct].freeze + FALLBACK_FINISH = 0 + OFFSET_BY_ONE = 1 # Each query should take < 500ms https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705 DEFAULT_DISTINCT_BATCH_SIZE = 10_000 @@ -65,7 +67,7 @@ module Gitlab (@operation == :count && batch_size <= MIN_REQUIRED_BATCH_SIZE) || (@operation == :sum && batch_size < DEFAULT_SUM_BATCH_SIZE) || (finish - start) / batch_size >= MAX_ALLOWED_LOOPS || - start > finish + start >= finish end def count(batch_size: nil, mode: :itself, start: nil, finish: nil) @@ -85,11 +87,13 @@ module Gitlab results = nil batch_start = start - while batch_start <= finish - batch_relation = build_relation_batch(batch_start, batch_start + batch_size, mode) + while batch_start < finish + batch_end = [batch_start + batch_size, finish].min + batch_relation = build_relation_batch(batch_start, batch_end, mode) + begin results = merge_results(results, batch_relation.send(@operation, *@operation_args)) # rubocop:disable GitlabSecurity/PublicSend - batch_start += batch_size + batch_start = batch_end rescue ActiveRecord::QueryCanceled => error # retry with a safe batch size & warmer cache if batch_size >= 2 * MIN_REQUIRED_BATCH_SIZE @@ -99,6 +103,7 @@ module Gitlab return FALLBACK end end + sleep(SLEEP_TIME_IN_SECONDS) end @@ -138,7 +143,7 @@ module Gitlab end def actual_finish(finish) - finish || @relation.unscope(:group, :having).maximum(@column) || 0 + (finish || @relation.unscope(:group, :having).maximum(@column) || FALLBACK_FINISH) + OFFSET_BY_ONE end def check_mode!(mode) diff --git a/lib/gitlab/database/migrations/background_migration_helpers.rb b/lib/gitlab/database/migrations/background_migration_helpers.rb index a6cc03aa9eb..36073844765 100644 --- a/lib/gitlab/database/migrations/background_migration_helpers.rb +++ b/lib/gitlab/database/migrations/background_migration_helpers.rb @@ -55,7 +55,8 @@ module Gitlab bulk_migrate_async(jobs) unless jobs.empty? end - # Queues background migration jobs for an entire table, batched by ID range. + # Queues background migration jobs for an entire table in batches. + # The default batching column used is the standard primary key `id`. # Each job is scheduled with a `delay_interval` in between. # If you use a small interval, then some jobs may run at the same time. # @@ -68,6 +69,7 @@ module Gitlab # is scheduled to be run. These records can be used to trace execution of the background job, but there is no # builtin support to manage that automatically at this time. You should only set this flag if you are aware of # how it works, and intend to manually cleanup the database records in your background job. + # primary_column_name - The name of the primary key column if the primary key is not `id` # # *Returns the final migration delay* # @@ -87,8 +89,9 @@ module Gitlab # # do something # end # end - def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE, other_job_arguments: [], initial_delay: 0, track_jobs: false) - raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id') + def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE, other_job_arguments: [], initial_delay: 0, track_jobs: false, primary_column_name: :id) + raise "#{model_class} does not have an ID column of #{primary_column_name} to use for batch ranges" unless model_class.column_names.include?(primary_column_name.to_s) + raise "#{primary_column_name} is not an integer column" unless model_class.columns_hash[primary_column_name.to_s].type == :integer # To not overload the worker too much we enforce a minimum interval both # when scheduling and performing jobs. @@ -99,7 +102,7 @@ module Gitlab final_delay = 0 model_class.each_batch(of: batch_size) do |relation, index| - start_id, end_id = relation.pluck(Arel.sql('MIN(id), MAX(id)')).first + start_id, end_id = relation.pluck(Arel.sql("MIN(#{primary_column_name}), MAX(#{primary_column_name})")).first # `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for # the same time, which is not helpful in most cases where we wish to diff --git a/lib/gitlab/database/postgres_hll/batch_distinct_counter.rb b/lib/gitlab/database/postgres_hll/batch_distinct_counter.rb new file mode 100644 index 00000000000..33faa2ef1b0 --- /dev/null +++ b/lib/gitlab/database/postgres_hll/batch_distinct_counter.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +module Gitlab + module Database + module PostgresHll + # For large tables, PostgreSQL can take a long time to count rows due to MVCC. + # Implements a distinct batch counter based on HyperLogLog algorithm + # Needs indexes on the column below to calculate max, min and range queries + # For larger tables just set higher batch_size with index optimization + # + # In order to not use a possible complex time consuming query when calculating min and max values, + # the start and finish can be sent specifically, start and finish should contain max and min values for PRIMARY KEY of + # relation (most cases `id` column) rather than counted attribute eg: + # estimate_distinct_count(start: ::Project.with_active_services.minimum(:id), finish: ::Project.with_active_services.maximum(:id)) + # + # Grouped relations are NOT supported yet. + # + # @example Usage + # ::Gitlab::Database::PostgresHllBatchDistinctCount.new(::Project, :creator_id).estimate_distinct_count + # ::Gitlab::Database::PostgresHllBatchDistinctCount.new(::Project.with_active_services.service_desk_enabled.where(time_period)) + # .estimate_distinct_count( + # batch_size: 1_000, + # start: ::Project.with_active_services.service_desk_enabled.where(time_period).minimum(:id), + # finish: ::Project.with_active_services.service_desk_enabled.where(time_period).maximum(:id) + # ) + # + # @note HyperLogLog is an PROBABILISTIC algorithm that ESTIMATES distinct count of given attribute value for supplied relation + # Like all probabilistic algorithm is has ERROR RATE margin, that can affect values, + # for given implementation no higher value was reported (https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45673#accuracy-estimation) than 5.3% + # for the most of a cases this value is lower. However, if the exact value is necessary other tools has to be used. + class BatchDistinctCounter + ERROR_RATE = 4.9 # max encountered empirical error rate, used in tests + FALLBACK = -1 + MIN_REQUIRED_BATCH_SIZE = 750 + SLEEP_TIME_IN_SECONDS = 0.01 # 10 msec sleep + MAX_DATA_VOLUME = 4_000_000_000 + + # Each query should take < 500ms https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705 + DEFAULT_BATCH_SIZE = 10_000 + + BIT_31_MASK = "B'0#{'1' * 31}'" + BIT_9_MASK = "B'#{'0' * 23}#{'1' * 9}'" + # @example source_query + # SELECT CAST(('X' || md5(CAST(%{column} as text))) as bit(32)) attr_hash_32_bits + # FROM %{relation} + # WHERE %{pkey} >= %{batch_start} + # AND %{pkey} < %{batch_end} + # AND %{column} IS NOT NULL + BUCKETED_DATA_SQL = <<~SQL + WITH hashed_attributes AS (%{source_query}) + SELECT (attr_hash_32_bits & #{BIT_9_MASK})::int AS bucket_num, + (31 - floor(log(2, min((attr_hash_32_bits & #{BIT_31_MASK})::int))))::int as bucket_hash + FROM hashed_attributes + GROUP BY 1 + SQL + + TOTAL_BUCKETS_NUMBER = 512 + + def initialize(relation, column = nil) + @relation = relation + @column = column || relation.primary_key + end + + def unwanted_configuration?(finish, batch_size, start) + batch_size <= MIN_REQUIRED_BATCH_SIZE || + (finish - start) >= MAX_DATA_VOLUME || + start > finish + end + + def estimate_distinct_count(batch_size: nil, start: nil, finish: nil) + raise 'BatchCount can not be run inside a transaction' if ActiveRecord::Base.connection.transaction_open? + + batch_size ||= DEFAULT_BATCH_SIZE + + start = actual_start(start) + finish = actual_finish(finish) + + raise "Batch counting expects positive values only for #{@column}" if start < 0 || finish < 0 + return FALLBACK if unwanted_configuration?(finish, batch_size, start) + + batch_start = start + hll_blob = {} + + while batch_start <= finish + begin + hll_blob.merge!(hll_blob_for_batch(batch_start, batch_start + batch_size)) {|_key, old, new| new > old ? new : old } + batch_start += batch_size + end + sleep(SLEEP_TIME_IN_SECONDS) + end + + estimate_cardinality(hll_blob) + end + + private + + # arbitrary values that are present in #estimate_cardinality + # are sourced from https://www.sisense.com/blog/hyperloglog-in-pure-sql/ + # article, they are not representing any entity and serves as tune value + # for the whole equation + def estimate_cardinality(hll_blob) + num_zero_buckets = TOTAL_BUCKETS_NUMBER - hll_blob.size + + num_uniques = ( + ((TOTAL_BUCKETS_NUMBER**2) * (0.7213 / (1 + 1.079 / TOTAL_BUCKETS_NUMBER))) / + (num_zero_buckets + hll_blob.values.sum { |bucket_hash| 2**(-1 * bucket_hash)} ) + ).to_i + + if num_zero_buckets > 0 && num_uniques < 2.5 * TOTAL_BUCKETS_NUMBER + ((0.7213 / (1 + 1.079 / TOTAL_BUCKETS_NUMBER)) * (TOTAL_BUCKETS_NUMBER * + Math.log2(TOTAL_BUCKETS_NUMBER.to_f / num_zero_buckets))) + else + num_uniques + end + end + + def hll_blob_for_batch(start, finish) + @relation + .connection + .execute(BUCKETED_DATA_SQL % { source_query: source_query(start, finish) }) + .map(&:values) + .to_h + end + + # Generate the source query SQL snippet for the provided id range + # + # @example SQL query template + # SELECT CAST(('X' || md5(CAST(%{column} as text))) as bit(32)) attr_hash_32_bits + # FROM %{relation} + # WHERE %{pkey} >= %{batch_start} AND %{pkey} < %{batch_end} + # AND %{column} IS NOT NULL + # + # @param start initial id range + # @param finish final id range + # @return [String] SQL query fragment + def source_query(start, finish) + col_as_arel = @column.is_a?(Arel::Attributes::Attribute) ? @column : Arel.sql(@column.to_s) + col_as_text = Arel::Nodes::NamedFunction.new('CAST', [col_as_arel.as('text')]) + md5_of_col = Arel::Nodes::NamedFunction.new('md5', [col_as_text]) + md5_as_hex = Arel::Nodes::Concat.new(Arel.sql("'X'"), md5_of_col) + bits = Arel::Nodes::NamedFunction.new('CAST', [md5_as_hex.as('bit(32)')]) + + @relation + .where(@relation.primary_key => (start...finish)) + .where(col_as_arel.not_eq(nil)) + .select(bits.as('attr_hash_32_bits')).to_sql + end + + def actual_start(start) + start || @relation.unscope(:group, :having).minimum(@relation.primary_key) || 0 + end + + def actual_finish(finish) + finish || @relation.unscope(:group, :having).maximum(@relation.primary_key) || 0 + end + end + end + end +end diff --git a/lib/gitlab/database/postgres_index.rb b/lib/gitlab/database/postgres_index.rb index 2a9f23f0098..6e734834841 100644 --- a/lib/gitlab/database/postgres_index.rb +++ b/lib/gitlab/database/postgres_index.rb @@ -3,9 +3,14 @@ module Gitlab module Database class PostgresIndex < ActiveRecord::Base + include Gitlab::Utils::StrongMemoize + self.table_name = 'postgres_indexes' self.primary_key = 'identifier' + has_one :bloat_estimate, class_name: 'Gitlab::Database::PostgresIndexBloatEstimate', foreign_key: :identifier + has_many :reindexing_actions, class_name: 'Gitlab::Database::Reindexing::ReindexAction', foreign_key: :index_identifier + scope :by_identifier, ->(identifier) do raise ArgumentError, "Index name is not fully qualified with a schema: #{identifier}" unless identifier =~ /^\w+\.\w+$/ @@ -17,11 +22,17 @@ module Gitlab # is defined on a table that is not partitioned. scope :regular, -> { where(unique: false, partitioned: false, exclusion: false)} - scope :random_few, ->(how_many) do - limit(how_many).order(Arel.sql('RANDOM()')) + scope :not_match, ->(regex) { where("name !~ ?", regex)} + + scope :not_recently_reindexed, -> do + recent_actions = Reindexing::ReindexAction.recent.where('index_identifier = identifier') + + where('NOT EXISTS (?)', recent_actions) end - scope :not_match, ->(regex) { where("name !~ ?", regex)} + def bloat_size + strong_memoize(:bloat_size) { bloat_estimate&.bloat_size || 0 } + end def to_s name diff --git a/lib/gitlab/database/postgres_index_bloat_estimate.rb b/lib/gitlab/database/postgres_index_bloat_estimate.rb new file mode 100644 index 00000000000..379227bf87c --- /dev/null +++ b/lib/gitlab/database/postgres_index_bloat_estimate.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Gitlab + module Database + # Use this model with care: Retrieving bloat statistics + # for all indexes can be expensive in a large database. + # + # Best used on a per-index basis. + class PostgresIndexBloatEstimate < ActiveRecord::Base + self.table_name = 'postgres_index_bloat_estimates' + self.primary_key = 'identifier' + + belongs_to :index, foreign_key: :identifier, class_name: 'Gitlab::Database::PostgresIndex' + + alias_attribute :bloat_size, :bloat_size_bytes + end + end +end diff --git a/lib/gitlab/database/postgresql_adapter/empty_query_ping.rb b/lib/gitlab/database/postgresql_adapter/empty_query_ping.rb new file mode 100644 index 00000000000..906312478ac --- /dev/null +++ b/lib/gitlab/database/postgresql_adapter/empty_query_ping.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# rubocop:disable Gitlab/ModuleWithInstanceVariables +module Gitlab + module Database + module PostgresqlAdapter + module EmptyQueryPing + # ActiveRecord uses `SELECT 1` to check if the connection is alive + # We patch this here to use an empty query instead, which is a bit faster + def active? + @lock.synchronize do + @connection.query ';' + end + true + rescue PG::Error + false + end + end + end + end +end +# rubocop:enable Gitlab/ModuleWithInstanceVariables diff --git a/lib/gitlab/database/reindexing.rb b/lib/gitlab/database/reindexing.rb index c77e000254f..832f7438cf9 100644 --- a/lib/gitlab/database/reindexing.rb +++ b/lib/gitlab/database/reindexing.rb @@ -3,8 +3,14 @@ module Gitlab module Database module Reindexing - def self.perform(index_selector) - Coordinator.new(index_selector).perform + # Number of indexes to reindex per invocation + DEFAULT_INDEXES_PER_INVOCATION = 2 + + # candidate_indexes: Array of Gitlab::Database::PostgresIndex + def self.perform(candidate_indexes, how_many: DEFAULT_INDEXES_PER_INVOCATION) + indexes = IndexSelection.new(candidate_indexes).take(how_many) + + Coordinator.new(indexes).perform end def self.candidate_indexes diff --git a/lib/gitlab/database/reindexing/concurrent_reindex.rb b/lib/gitlab/database/reindexing/concurrent_reindex.rb index fd3dca88567..a6fe7d61a4f 100644 --- a/lib/gitlab/database/reindexing/concurrent_reindex.rb +++ b/lib/gitlab/database/reindexing/concurrent_reindex.rb @@ -59,6 +59,13 @@ module Gitlab raise ReindexError, "failed to reindex #{index}: #{message}" end + # Some expression indexes (aka functional indexes) + # require additional statistics. The existing statistics + # are tightly bound to the original index. We have to + # rebuild statistics for the new index before dropping + # the original one. + rebuild_statistics if index.expression? + yield replacement_index ensure begin @@ -96,6 +103,14 @@ module Gitlab end end + def rebuild_statistics + logger.info("rebuilding table statistics for #{index.schema}.#{index.tablename}") + + connection.execute(<<~SQL) + ANALYZE #{quote_table_name(index.schema)}.#{quote_table_name(index.tablename)} + SQL + end + def replacement_index_name @replacement_index_name ||= "#{TEMPORARY_INDEX_PREFIX}#{index.indexrelid}" end diff --git a/lib/gitlab/database/reindexing/index_selection.rb b/lib/gitlab/database/reindexing/index_selection.rb new file mode 100644 index 00000000000..406e70791df --- /dev/null +++ b/lib/gitlab/database/reindexing/index_selection.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + module Database + module Reindexing + class IndexSelection + include Enumerable + + delegate :each, to: :indexes + + def initialize(candidates) + @candidates = candidates + end + + private + + attr_reader :candidates + + def indexes + # This is an explicit N+1 query: + # Bloat estimates are generally available through a view + # for all indexes. However, estimating bloat for all + # indexes at once is an expensive operation. Therefore, + # we force a N+1 pattern here and estimate bloat on a per-index + # basis. + + @indexes ||= filter_candidates.sort_by(&:bloat_size).reverse + end + + def filter_candidates + candidates.not_recently_reindexed + end + end + end + end +end diff --git a/lib/gitlab/database/reindexing/reindex_action.rb b/lib/gitlab/database/reindexing/reindex_action.rb index 0928ef90e5d..8c59cffe5fb 100644 --- a/lib/gitlab/database/reindexing/reindex_action.rb +++ b/lib/gitlab/database/reindexing/reindex_action.rb @@ -6,13 +6,20 @@ module Gitlab class ReindexAction < ActiveRecord::Base self.table_name = 'postgres_reindex_actions' + belongs_to :index, foreign_key: :index_identifier, class_name: 'Gitlab::Database::PostgresIndex' enum state: { started: 0, finished: 1, failed: 2 } + # Amount of time to consider a previous reindexing *recent* + RECENT_THRESHOLD = 7.days + + scope :recent, -> { where(state: :finished).where('action_end > ?', Time.zone.now - RECENT_THRESHOLD) } + def self.keep_track_of(index, &block) action = create!( index_identifier: index.identifier, action_start: Time.zone.now, - ondisk_size_bytes_start: index.ondisk_size_bytes + ondisk_size_bytes_start: index.ondisk_size_bytes, + bloat_estimate_bytes_start: index.bloat_size ) yield diff --git a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb index 88f035c2d1b..b1093b2fca4 100644 --- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb +++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb @@ -147,7 +147,7 @@ module Gitlab initialize_with_readme: true, visibility_level: VISIBILITY_LEVEL, name: PROJECT_NAME, - description: "This project is automatically generated and will be used to help monitor this GitLab instance. [More information](#{docs_path})", + description: "This project is automatically generated and helps monitor this GitLab instance. [Learn more](#{docs_path}).", namespace_id: group.id } end diff --git a/lib/gitlab/deploy_key_access.rb b/lib/gitlab/deploy_key_access.rb new file mode 100644 index 00000000000..ca16582d2b4 --- /dev/null +++ b/lib/gitlab/deploy_key_access.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Gitlab + class DeployKeyAccess < UserAccess + def initialize(deploy_key, container: nil) + @deploy_key = deploy_key + @user = deploy_key.user + @container = container + end + + def can_push_for_ref?(ref) + can_push_to_branch?(ref) + end + + private + + attr_reader :deploy_key + + def protected_tag_accessible_to?(ref, action:) + assert_project! + + # a deploy key can always push a protected tag + # (which is not always the case when pushing to a protected branch) + true + end + + def can_collaborate?(_ref) + assert_project! + + project_has_active_user_keys? + end + + def project_has_active_user_keys? + user.can?(:read_project, project) && DeployKey.with_write_access_for_project(project).id_in(deploy_key.id).exists? + end + end +end diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index cf0611e44da..8f4f8febec0 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -30,12 +30,16 @@ module Gitlab @diffs ||= diffable.raw_diffs(diff_options) end - def diff_files - raw_diff_files + def diff_files(sorted: false) + raw_diff_files(sorted: sorted) end - def raw_diff_files - @raw_diff_files ||= diffs.decorate! { |diff| decorate_diff!(diff) } + def raw_diff_files(sorted: false) + strong_memoize(:"raw_diff_files_#{sorted}") do + collection = diffs.decorate! { |diff| decorate_diff!(diff) } + collection = sort_diffs(collection) if sorted + collection + end end def diff_file_paths @@ -111,6 +115,12 @@ module Gitlab fallback_diff_refs: fallback_diff_refs, stats: stats) end + + def sort_diffs(diffs) + return diffs unless Feature.enabled?(:sort_diffs, project, default_enabled: false) + + Gitlab::Diff::FileCollectionSorter.new(diffs).sort + end end end end diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb index 16257bb5ff5..d2ca86fdfe7 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb @@ -16,7 +16,7 @@ module Gitlab fallback_diff_refs: merge_request_diff.fallback_diff_refs) end - def diff_files + def diff_files(sorted: false) strong_memoize(:diff_files) do diff_files = super @@ -26,6 +26,12 @@ module Gitlab end end + def raw_diff_files(sorted: false) + # We force `sorted` to `false` as we don't need to sort the diffs when + # dealing with `MergeRequestDiff` since we sort its files on create. + super(sorted: false) + end + override :write_cache def write_cache highlight_cache.write_if_empty diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_batch.rb b/lib/gitlab/diff/file_collection/merge_request_diff_batch.rb index 9af66318b89..64523f3b730 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff_batch.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff_batch.rb @@ -11,7 +11,7 @@ module Gitlab # class MergeRequestDiffBatch < MergeRequestDiffBase DEFAULT_BATCH_PAGE = 1 - DEFAULT_BATCH_SIZE = 20 + DEFAULT_BATCH_SIZE = 30 attr_reader :pagination_data @@ -21,9 +21,9 @@ module Gitlab @paginated_collection = load_paginated_collection(batch_page, batch_size, diff_options) @pagination_data = { - current_page: @paginated_collection.current_page, - next_page: @paginated_collection.next_page, - total_pages: @paginated_collection.total_pages + current_page: batch_gradual_load? ? nil : @paginated_collection.current_page, + next_page: batch_gradual_load? ? nil : @paginated_collection.next_page, + total_pages: batch_gradual_load? ? relation.size : @paginated_collection.total_pages } end @@ -62,17 +62,28 @@ module Gitlab @merge_request_diff.merge_request_diff_files end + # rubocop: disable CodeReuse/ActiveRecord def load_paginated_collection(batch_page, batch_size, diff_options) batch_page ||= DEFAULT_BATCH_PAGE batch_size ||= DEFAULT_BATCH_SIZE paths = diff_options&.fetch(:paths, nil) - paginated_collection = relation.page(batch_page).per(batch_size) + paginated_collection = if batch_gradual_load? + relation.offset(batch_page).limit([batch_size.to_i, DEFAULT_BATCH_SIZE].min) + else + relation.page(batch_page).per(batch_size) + end + paginated_collection = paginated_collection.by_paths(paths) if paths paginated_collection end + # rubocop: enable CodeReuse/ActiveRecord + + def batch_gradual_load? + Feature.enabled?(:diffs_gradual_load, @merge_request_diff.project, default_enabled: true) + end end end end diff --git a/lib/gitlab/diff/file_collection_sorter.rb b/lib/gitlab/diff/file_collection_sorter.rb new file mode 100644 index 00000000000..94626875580 --- /dev/null +++ b/lib/gitlab/diff/file_collection_sorter.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Gitlab + module Diff + class FileCollectionSorter + attr_reader :diffs + + def initialize(diffs) + @diffs = diffs + end + + def sort + diffs.sort do |a, b| + compare_path_parts(path_parts(a), path_parts(b)) + end + end + + private + + def path_parts(diff) + (diff.new_path.presence || diff.old_path).split(::File::SEPARATOR) + end + + # Used for sorting the file paths by: + # 1. Directory name + # 2. Depth + # 3. File name + def compare_path_parts(a_parts, b_parts) + a_part = a_parts.shift + b_part = b_parts.shift + + return 1 if a_parts.size < b_parts.size && a_parts.empty? + return -1 if a_parts.size > b_parts.size && b_parts.empty? + + comparison = a_part <=> b_part + + return comparison unless comparison == 0 + + compare_path_parts(a_parts, b_parts) + end + end + end +end diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb index 1beea4f9054..9e476dd4e2b 100644 --- a/lib/gitlab/email/handler/reply_processing.rb +++ b/lib/gitlab/email/handler/reply_processing.rb @@ -45,7 +45,7 @@ module Gitlab end def add_attachments(reply) - attachments = Email::AttachmentUploader.new(mail).execute(upload_params) + attachments = Email::AttachmentUploader.new(mail).execute(**upload_params) reply + attachments.map do |link| "\n\n#{link[:markdown]}" diff --git a/lib/gitlab/email/handler/service_desk_handler.rb b/lib/gitlab/email/handler/service_desk_handler.rb index bcd8b98a06f..0bbe3980f67 100644 --- a/lib/gitlab/email/handler/service_desk_handler.rb +++ b/lib/gitlab/email/handler/service_desk_handler.rb @@ -68,7 +68,7 @@ module Gitlab end def valid_project_key?(project, slug) - project.present? && slug == project.full_path_slug && Feature.enabled?(:service_desk_custom_address, project) + project.present? && slug == project.full_path_slug && Feature.enabled?(:service_desk_custom_address, project, default_enabled: true) end def create_issue! @@ -78,7 +78,7 @@ module Gitlab title: issue_title, description: message_including_template, confidential: true, - service_desk_reply_to: from_address + external_author: from_address ).execute raise InvalidIssueError unless @issue.persisted? diff --git a/lib/gitlab/encrypted_configuration.rb b/lib/gitlab/encrypted_configuration.rb new file mode 100644 index 00000000000..fe49af3ab33 --- /dev/null +++ b/lib/gitlab/encrypted_configuration.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +module Gitlab + class EncryptedConfiguration + delegate :[], :fetch, to: :config + delegate_missing_to :options + attr_reader :content_path, :key, :previous_keys + + CIPHER = "aes-256-gcm" + SALT = "GitLabEncryptedConfigSalt" + + class MissingKeyError < RuntimeError + def initialize(msg = "Missing encryption key to encrypt/decrypt file with.") + super + end + end + + class InvalidConfigError < RuntimeError + def initialize(msg = "Content was not a valid yml config file") + super + end + end + + def self.generate_key(base_key) + # Because the salt is static, we want uniqueness to be coming from the base_key + # Error if the base_key is empty or suspiciously short + raise 'Base key too small' if base_key.blank? || base_key.length < 16 + + ActiveSupport::KeyGenerator.new(base_key).generate_key(SALT, ActiveSupport::MessageEncryptor.key_len(CIPHER)) + end + + def initialize(content_path: nil, base_key: nil, previous_keys: []) + @content_path = Pathname.new(content_path).yield_self { |path| path.symlink? ? path.realpath : path } if content_path + @key = self.class.generate_key(base_key) if base_key + @previous_keys = previous_keys + end + + def active? + content_path&.exist? + end + + def read + if active? + decrypt(content_path.binread) + else + "" + end + end + + def write(contents) + # ensure contents are valid to deserialize before write + deserialize(contents) + + temp_file = Tempfile.new(File.basename(content_path), File.dirname(content_path)) + File.open(temp_file.path, 'wb') do |file| + file.write(encrypt(contents)) + end + FileUtils.mv(temp_file.path, content_path) + ensure + temp_file&.unlink + end + + def config + return @config if @config + + contents = deserialize(read) + + raise InvalidConfigError.new unless contents.is_a?(Hash) + + @config = contents.deep_symbolize_keys + end + + def change(&block) + writing(read, &block) + end + + private + + def writing(contents) + updated_contents = yield contents + + write(updated_contents) if updated_contents != contents + end + + def encrypt(contents) + handle_missing_key! + encryptor.encrypt_and_sign(contents) + end + + def decrypt(contents) + handle_missing_key! + encryptor.decrypt_and_verify(contents) + end + + def encryptor + return @encryptor if @encryptor + + @encryptor = ActiveSupport::MessageEncryptor.new(key, cipher: CIPHER) + + # Allow fallback to previous keys + @previous_keys.each do |key| + @encryptor.rotate(self.class.generate_key(key)) + end + + @encryptor + end + + def options + # Allows top level keys to be referenced using dot syntax + @options ||= ActiveSupport::InheritableOptions.new(config) + end + + def deserialize(contents) + YAML.safe_load(contents, permitted_classes: [Symbol]).presence || {} + end + + def handle_missing_key! + raise MissingKeyError.new if @key.nil? + end + end +end diff --git a/lib/gitlab/encrypted_ldap_command.rb b/lib/gitlab/encrypted_ldap_command.rb new file mode 100644 index 00000000000..cdb3e268b51 --- /dev/null +++ b/lib/gitlab/encrypted_ldap_command.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +# rubocop:disable Rails/Output +module Gitlab + class EncryptedLdapCommand + class << self + def write(contents) + encrypted = Gitlab::Auth::Ldap::Config.encrypted_secrets + return unless validate_config(encrypted) + + validate_contents(contents) + encrypted.write(contents) + + puts "File encrypted and saved." + rescue Interrupt + puts "Aborted changing file: nothing saved." + rescue ActiveSupport::MessageEncryptor::InvalidMessage + puts "Couldn't decrypt #{encrypted.content_path}. Perhaps you passed the wrong key?" + end + + def edit + encrypted = Gitlab::Auth::Ldap::Config.encrypted_secrets + return unless validate_config(encrypted) + + if ENV["EDITOR"].blank? + puts 'No $EDITOR specified to open file. Please provide one when running the command:' + puts 'gitlab-rake gitlab:ldap:secret:edit EDITOR=vim' + return + end + + temp_file = Tempfile.new(File.basename(encrypted.content_path), File.dirname(encrypted.content_path)) + contents_changed = false + + encrypted.change do |contents| + contents = encrypted_file_template unless File.exist?(encrypted.content_path) + File.write(temp_file.path, contents) + system(ENV['EDITOR'], temp_file.path) + changes = File.read(temp_file.path) + contents_changed = contents != changes + validate_contents(changes) + changes + end + + puts "Contents were unchanged." unless contents_changed + puts "File encrypted and saved." + rescue Interrupt + puts "Aborted changing file: nothing saved." + rescue ActiveSupport::MessageEncryptor::InvalidMessage + puts "Couldn't decrypt #{encrypted.content_path}. Perhaps you passed the wrong key?" + ensure + temp_file&.unlink + end + + def show + encrypted = Gitlab::Auth::Ldap::Config.encrypted_secrets + return unless validate_config(encrypted) + + puts encrypted.read.presence || "File '#{encrypted.content_path}' does not exist. Use `gitlab-rake gitlab:ldap:secret:edit` to change that." + rescue ActiveSupport::MessageEncryptor::InvalidMessage + puts "Couldn't decrypt #{encrypted.content_path}. Perhaps you passed the wrong key?" + end + + private + + def validate_config(encrypted) + dir_path = File.dirname(encrypted.content_path) + + unless File.exist?(dir_path) + puts "Directory #{dir_path} does not exist. Create the directory and try again." + return false + end + + if encrypted.key.nil? + puts "Missing encryption key encrypted_settings_key_base." + return false + end + + true + end + + def validate_contents(contents) + begin + config = YAML.safe_load(contents, permitted_classes: [Symbol]) + error_contents = "Did not include any key-value pairs" unless config.is_a?(Hash) + rescue Psych::Exception => e + error_contents = e.message + end + + puts "WARNING: Content was not a valid LDAP secret yml file. #{error_contents}" if error_contents + + contents + end + + def encrypted_file_template + <<~YAML + # main: + # password: '123' + # user_dn: 'gitlab-adm' + YAML + end + end + end +end +# rubocop:enable Rails/Output diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 6e39776bbd4..94523813662 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -4,9 +4,11 @@ # # Utility module for A/B testing experimental features. Define your experiments in the `EXPERIMENTS` constant. # Experiment options: -# - environment (optional, defaults to enabled for development and GitLab.com) # - tracking_category (optional, used to set the category when tracking an experiment event) -# - use_backwards_compatible_subject_index (optional, set this to true if you need backwards compatibility) +# - use_backwards_compatible_subject_index (optional, set this to true if you need backwards compatibility -- you likely do not need this, see note in the next paragraph.) +# +# Using the backwards-compatible subject index (use_backwards_compatible_subject_index option): +# This option was added when [the calculation of experimentation_subject_index was changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45733/diffs#41af4a6fa5a10c7068559ce21c5188483751d934_157_173). It is not intended to be used by new experiments, it exists merely for the segmentation integrity of in-flight experiments at the time the change was deployed. That is, we want users who were assigned to the "experimental" group or the "control" group before the change to still be in those same groups after the change. See [the original issue](https://gitlab.com/gitlab-org/gitlab/-/issues/270858) and [this related comment](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48110#note_458223745) for more information. # # The experiment is controlled by a Feature Flag (https://docs.gitlab.com/ee/development/feature_flags/controls.html), # which is named "#{experiment_key}_experiment_percentage" and *must* be set with a percentage and not be used for other purposes. @@ -55,10 +57,6 @@ module Gitlab tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyGroupVersionA', use_backwards_compatible_subject_index: true }, - new_create_project_ui: { - tracking_category: 'Manage::Import::Experiment::NewCreateProjectUi', - use_backwards_compatible_subject_index: true - }, contact_sales_btn_in_app: { tracking_category: 'Growth::Conversion::Experiment::ContactSalesInApp', use_backwards_compatible_subject_index: true @@ -67,14 +65,6 @@ module Gitlab tracking_category: 'Growth::Expansion::Experiment::CustomizeHomepage', use_backwards_compatible_subject_index: true }, - invite_email: { - tracking_category: 'Growth::Acquisition::Experiment::InviteEmail', - use_backwards_compatible_subject_index: true - }, - invitation_reminders: { - tracking_category: 'Growth::Acquisition::Experiment::InvitationReminders', - use_backwards_compatible_subject_index: true - }, group_only_trials: { tracking_category: 'Growth::Conversion::Experiment::GroupOnlyTrials', use_backwards_compatible_subject_index: true @@ -82,59 +72,68 @@ module Gitlab default_to_issues_board: { tracking_category: 'Growth::Conversion::Experiment::DefaultToIssuesBoard', use_backwards_compatible_subject_index: true + }, + jobs_empty_state: { + tracking_category: 'Growth::Activation::Experiment::JobsEmptyState' + }, + remove_known_trial_form_fields: { + tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFields' + }, + trimmed_skip_trial_copy: { + tracking_category: 'Growth::Conversion::Experiment::TrimmedSkipTrialCopy' + }, + trial_registration_with_social_signin: { + tracking_category: 'Growth::Conversion::Experiment::TrialRegistrationWithSocialSigning' + }, + invite_members_empty_project_version_a: { + tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyProjectVersionA' } }.freeze class << self - def experiment(key) - Experiment.new(EXPERIMENTS[key].merge(key: key)) - end - - def enabled?(experiment_key) - return false unless EXPERIMENTS.key?(experiment_key) + def get_experiment(experiment_key) + return unless EXPERIMENTS.key?(experiment_key) - experiment = experiment(experiment_key) - experiment.enabled_for_environment? && experiment.enabled? + ::Gitlab::Experimentation::Experiment.new(experiment_key, **EXPERIMENTS[experiment_key]) end - def enabled_for_attribute?(experiment_key, attribute) - index = Digest::SHA1.hexdigest(attribute).hex % 100 - enabled_for_value?(experiment_key, index) - end + def active?(experiment_key) + experiment = get_experiment(experiment_key) + return false unless experiment - def enabled_for_value?(experiment_key, value) - enabled?(experiment_key) && experiment(experiment_key).enabled_for_index?(value) + experiment.active? end - end - Experiment = Struct.new( - :key, - :environment, - :tracking_category, - :use_backwards_compatible_subject_index, - keyword_init: true - ) do - def enabled? - experiment_percentage > 0 - end + def in_experiment_group?(experiment_key, subject:) + return false if subject.blank? + return false unless active?(experiment_key) - def enabled_for_environment? - return ::Gitlab.dev_env_or_com? if environment.nil? + experiment = get_experiment(experiment_key) + return false unless experiment - environment + experiment.enabled_for_index?(index_for_subject(experiment, subject)) end - def enabled_for_index?(index) - return false if index.blank? + private - index <= experiment_percentage - end + def index_for_subject(experiment, subject) + index = if experiment.use_backwards_compatible_subject_index + Digest::SHA1.hexdigest(subject_id(subject)).hex + else + Zlib.crc32("#{experiment.key}#{subject_id(subject)}") + end - private + index % 100 + end - # When a feature does not exist, the `percentage_of_time_value` method will return 0 - def experiment_percentage - @experiment_percentage ||= Feature.get(:"#{key}_experiment_percentage").percentage_of_time_value # rubocop:disable Gitlab/AvoidFeatureGet + def subject_id(subject) + if subject.respond_to?(:to_global_id) + subject.to_global_id.to_s + elsif subject.respond_to?(:to_s) + subject.to_s + else + raise ArgumentError.new('Subject must respond to `to_global_id` or `to_s`') + end end end end diff --git a/lib/gitlab/experimentation/controller_concern.rb b/lib/gitlab/experimentation/controller_concern.rb index c6d15d7d82d..c85d3f4eee6 100644 --- a/lib/gitlab/experimentation/controller_concern.rb +++ b/lib/gitlab/experimentation/controller_concern.rb @@ -3,7 +3,7 @@ require 'zlib' # Controller concern that checks if an `experimentation_subject_id cookie` is present and sets it if absent. -# Used for A/B testing of experimental features. Exposes the `experiment_enabled?(experiment_name)` method +# Used for A/B testing of experimental features. Exposes the `experiment_enabled?(experiment_name, subject: nil)` method # to controllers and views. It returns true when the experiment is enabled and the user is selected as part # of the experimental group. # @@ -28,47 +28,56 @@ module Gitlab } end - def push_frontend_experiment(experiment_key) + def push_frontend_experiment(experiment_key, subject: nil) var_name = experiment_key.to_s.camelize(:lower) - enabled = experiment_enabled?(experiment_key) + + enabled = experiment_enabled?(experiment_key, subject: subject) gon.push({ experiments: { var_name => enabled } }, true) end - def experiment_enabled?(experiment_key) + def experiment_enabled?(experiment_key, subject: nil) + return true if forced_enabled?(experiment_key) return false if dnt_enabled? - return true if Experimentation.enabled_for_value?(experiment_key, experimentation_subject_index(experiment_key)) - return true if forced_enabled?(experiment_key) + subject ||= fallback_experimentation_subject_index(experiment_key) - false + Experimentation.in_experiment_group?(experiment_key, subject: subject) end - def track_experiment_event(experiment_key, action, value = nil) + def track_experiment_event(experiment_key, action, value = nil, subject: nil) return if dnt_enabled? - track_experiment_event_for(experiment_key, action, value) do |tracking_data| + track_experiment_event_for(experiment_key, action, value, subject: subject) do |tracking_data| ::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), **tracking_data) end end - def frontend_experimentation_tracking_data(experiment_key, action, value = nil) + def frontend_experimentation_tracking_data(experiment_key, action, value = nil, subject: nil) return if dnt_enabled? - track_experiment_event_for(experiment_key, action, value) do |tracking_data| + track_experiment_event_for(experiment_key, action, value, subject: subject) do |tracking_data| gon.push(tracking_data: tracking_data) end end - def record_experiment_user(experiment_key) + def record_experiment_user(experiment_key, context = {}) + return if dnt_enabled? + return unless Experimentation.active?(experiment_key) && current_user + + ::Experiment.add_user(experiment_key, tracking_group(experiment_key, nil, subject: current_user), current_user, context) + end + + def record_experiment_conversion_event(experiment_key) return if dnt_enabled? - return unless Experimentation.enabled?(experiment_key) && current_user + return unless current_user + return unless Experimentation.active?(experiment_key) - ::Experiment.add_user(experiment_key, tracking_group(experiment_key), current_user) + ::Experiment.record_conversion_event(experiment_key, current_user) end - def experiment_tracking_category_and_group(experiment_key) - "#{tracking_category(experiment_key)}:#{tracking_group(experiment_key, '_group')}" + def experiment_tracking_category_and_group(experiment_key, subject: nil) + "#{tracking_category(experiment_key)}:#{tracking_group(experiment_key, '_group', subject: subject)}" end private @@ -81,40 +90,41 @@ module Gitlab cookies.signed[:experimentation_subject_id] end - def experimentation_subject_index(experiment_key) + def fallback_experimentation_subject_index(experiment_key) return if experimentation_subject_id.blank? - if Experimentation.experiment(experiment_key).use_backwards_compatible_subject_index - experimentation_subject_id.delete('-').hex % 100 + if Experimentation.get_experiment(experiment_key).use_backwards_compatible_subject_index + experimentation_subject_id.delete('-') else - Zlib.crc32("#{experiment_key}#{experimentation_subject_id}") % 100 + experimentation_subject_id end end - def track_experiment_event_for(experiment_key, action, value) - return unless Experimentation.enabled?(experiment_key) + def track_experiment_event_for(experiment_key, action, value, subject: nil) + return unless Experimentation.active?(experiment_key) - yield experimentation_tracking_data(experiment_key, action, value) + yield experimentation_tracking_data(experiment_key, action, value, subject: subject) end - def experimentation_tracking_data(experiment_key, action, value) + def experimentation_tracking_data(experiment_key, action, value, subject: nil) { category: tracking_category(experiment_key), action: action, - property: tracking_group(experiment_key, "_group"), - label: experimentation_subject_id, + property: tracking_group(experiment_key, "_group", subject: subject), + label: tracking_label(subject), value: value }.compact end def tracking_category(experiment_key) - Experimentation.experiment(experiment_key).tracking_category + Experimentation.get_experiment(experiment_key).tracking_category end - def tracking_group(experiment_key, suffix = nil) - return unless Experimentation.enabled?(experiment_key) + def tracking_group(experiment_key, suffix = nil, subject: nil) + return unless Experimentation.active?(experiment_key) - group = experiment_enabled?(experiment_key) ? GROUP_EXPERIMENTAL : GROUP_CONTROL + subject ||= fallback_experimentation_subject_index(experiment_key) + group = experiment_enabled?(experiment_key, subject: subject) ? GROUP_EXPERIMENTAL : GROUP_CONTROL suffix ? "#{group}#{suffix}" : group end @@ -122,6 +132,16 @@ module Gitlab def forced_enabled?(experiment_key) params.has_key?(:force_experiment) && params[:force_experiment] == experiment_key.to_s end + + def tracking_label(subject) + return experimentation_subject_id if subject.blank? + + if subject.respond_to?(:to_global_id) + Digest::MD5.hexdigest(subject.to_global_id.to_s) + else + Digest::MD5.hexdigest(subject.to_s) + end + end end end end diff --git a/lib/gitlab/experimentation/experiment.rb b/lib/gitlab/experimentation/experiment.rb new file mode 100644 index 00000000000..e594c3bedeb --- /dev/null +++ b/lib/gitlab/experimentation/experiment.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Experimentation + class Experiment + attr_reader :key, :tracking_category, :use_backwards_compatible_subject_index + + def initialize(key, **params) + @key = key + @tracking_category = params[:tracking_category] + @use_backwards_compatible_subject_index = params[:use_backwards_compatible_subject_index] + + @experiment_percentage = Feature.get(:"#{key}_experiment_percentage").percentage_of_time_value # rubocop:disable Gitlab/AvoidFeatureGet + end + + def active? + ::Gitlab.dev_env_or_com? && experiment_percentage > 0 + end + + def enabled_for_index?(index) + return false if index.blank? + + index <= experiment_percentage + end + + private + + attr_reader :experiment_percentage + end + end +end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 96f3487fd6f..a2215366bdc 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -17,6 +17,7 @@ module Gitlab CommitError = Class.new(BaseError) OSError = Class.new(BaseError) UnknownRef = Class.new(BaseError) + CommandTimedOut = Class.new(CommandError) class << self include Gitlab::EncodingHelper diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index 6090d1b9f69..8df4bc3de05 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -66,6 +66,12 @@ module Gitlab @iterator = nil end + def sort(&block) + @array = @array.sort(&block) + + self + end + def empty? any? # Make sure the iterator has been exercised @empty diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index bc712e87e99..f6601379202 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -467,6 +467,18 @@ module Gitlab empty_diff_stats end + def find_changed_paths(commits) + processed_commits = commits.reject { |ref| ref.blank? || Gitlab::Git.blank_ref?(ref) } + + return [] if processed_commits.empty? + + wrapped_gitaly_errors do + gitaly_commit_client.find_changed_paths(processed_commits) + end + rescue CommandError, TypeError, NoRepository + [] + end + # Returns a RefName for a given SHA def ref_name_for_sha(ref_path, sha) raise ArgumentError, "sha can't be empty" unless sha.present? diff --git a/lib/gitlab/git/wraps_gitaly_errors.rb b/lib/gitlab/git/wraps_gitaly_errors.rb index 9963bcfbf1c..2009683d32c 100644 --- a/lib/gitlab/git/wraps_gitaly_errors.rb +++ b/lib/gitlab/git/wraps_gitaly_errors.rb @@ -9,6 +9,8 @@ module Gitlab raise Gitlab::Git::Repository::NoRepository.new(e) rescue GRPC::InvalidArgument => e raise ArgumentError.new(e) + rescue GRPC::DeadlineExceeded => e + raise Gitlab::Git::CommandTimedOut.new(e) rescue GRPC::BadStatus => e raise Gitlab::Git::CommandError.new(e) end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 0576d1dd9db..e0b145f69aa 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -43,7 +43,7 @@ module Gitlab ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS attr_reader :actor, :protocol, :authentication_abilities, - :namespace_path, :redirected_path, :auth_result_type, + :repository_path, :redirected_path, :auth_result_type, :cmd, :changes attr_accessor :container @@ -57,21 +57,16 @@ module Gitlab raise ArgumentError, "No error message defined for #{key}" end - def initialize(actor, container, protocol, authentication_abilities:, namespace_path: nil, repository_path: nil, redirected_path: nil, auth_result_type: nil) + def initialize(actor, container, protocol, authentication_abilities:, repository_path: nil, redirected_path: nil, auth_result_type: nil) @actor = actor @container = container @protocol = protocol @authentication_abilities = Array(authentication_abilities) - @namespace_path = namespace_path @repository_path = repository_path @redirected_path = redirected_path @auth_result_type = auth_result_type end - def repository_path - @repository_path ||= project&.path - end - def check(cmd, changes) @changes = changes @cmd = cmd @@ -82,6 +77,7 @@ module Gitlab check_authentication_abilities! check_command_disabled! check_command_existence! + check_otp_session! custom_action = check_custom_action return custom_action if custom_action @@ -259,6 +255,31 @@ module Gitlab end end + def check_otp_session! + return unless ssh? + return if !key? || deploy_key? + return unless Feature.enabled?(:two_factor_for_cli) + return unless user.two_factor_enabled? + + if ::Gitlab::Auth::Otp::SessionEnforcer.new(actor).access_restricted? + message = "OTP verification is required to access the repository.\n\n"\ + " Use: #{build_ssh_otp_verify_command}" + + raise ForbiddenError, message + end + end + + def build_ssh_otp_verify_command + user = "#{Gitlab.config.gitlab_shell.ssh_user}@" unless Gitlab.config.gitlab_shell.ssh_user.empty? + user_host = "#{user}#{Gitlab.config.gitlab_shell.ssh_host}" + + if Gitlab.config.gitlab_shell.ssh_port != 22 + "ssh #{user_host} -p #{Gitlab.config.gitlab_shell.ssh_port} 2fa_verify" + else + "ssh #{user_host} 2fa_verify" + end + end + def check_db_accessibility! return unless receive_pack? @@ -324,11 +345,11 @@ module Gitlab end def check_change_access! - # Deploy keys with write access can push anything - return if deploy_key? + return if deploy_key? && !deploy_keys_on_protected_branches_enabled? if changes == ANY - can_push = user_can_push? || + can_push = (deploy_key? && deploy_keys_on_protected_branches_enabled?) || + user_can_push? || project&.any_branch_allows_collaboration?(user_access.user) unless can_push @@ -404,6 +425,10 @@ module Gitlab protocol == 'http' end + def ssh? + protocol == 'ssh' + end + def upload_pack? cmd == 'git-upload-pack' end @@ -454,6 +479,8 @@ module Gitlab CiAccess.new elsif user && request_from_ci_build? BuildAccess.new(user, container: container) + elsif deploy_key? && deploy_keys_on_protected_branches_enabled? + DeployKeyAccess.new(deploy_key, container: container) else UserAccess.new(user, container: container) end @@ -531,6 +558,10 @@ module Gitlab def size_checker container.repository_size_checker end + + def deploy_keys_on_protected_branches_enabled? + Feature.enabled?(:deploy_keys_on_protected_branches, project) + end end end diff --git a/lib/gitlab/git_access_project.rb b/lib/gitlab/git_access_project.rb index cdefcc84f7d..7e9bab4a8e6 100644 --- a/lib/gitlab/git_access_project.rb +++ b/lib/gitlab/git_access_project.rb @@ -35,7 +35,19 @@ module Gitlab end def namespace - @namespace ||= Namespace.find_by_full_path(namespace_path) + strong_memoize(:namespace) { Namespace.find_by_full_path(namespace_path) } + end + + def namespace_path + strong_memoize(:namespace_path) { repository_path_match[:namespace_path] } + end + + def project_path + strong_memoize(:project_path) { repository_path_match[:project_path] } + end + + def repository_path_match + strong_memoize(:repository_path_match) { repository_path.match(Gitlab::PathRegex.full_project_git_path_regex) || {} } end def ensure_project_on_push! @@ -44,7 +56,7 @@ module Gitlab return unless user&.can?(:create_projects, namespace) project_params = { - path: repository_path, + path: project_path, namespace_id: namespace.id, visibility_level: Gitlab::VisibilityLevel::PRIVATE } diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb index 710e2ce90ec..854bf6e9c9e 100644 --- a/lib/gitlab/git_access_snippet.rb +++ b/lib/gitlab/git_access_snippet.rb @@ -114,7 +114,7 @@ module Gitlab override :check_single_change_access def check_single_change_access(change, _skip_lfs_integrity_check: false) - Checks::SnippetCheck.new(change, default_branch: snippet.default_branch, logger: logger).validate! + Checks::SnippetCheck.new(change, default_branch: snippet.default_branch, root_ref: snippet.repository.root_ref, logger: logger).validate! Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit, logger: logger).validate! rescue Checks::TimedLogger::TimeoutError raise TimeoutError, logger.full_message diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 464d2519b27..599bce176c9 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -216,6 +216,23 @@ module Gitlab response.flat_map(&:stats) end + def find_changed_paths(commits) + request = Gitaly::FindChangedPathsRequest.new( + repository: @gitaly_repo, + commits: commits + ) + + response = GitalyClient.call(@repository.storage, :diff_service, :find_changed_paths, request, timeout: GitalyClient.medium_timeout) + response.flat_map do |msg| + msg.paths.map do |path| + OpenStruct.new( + status: path.status, + path: EncodingHelper.encode!(path.path) + ) + end + end + end + def find_all_commits(opts = {}) request = Gitaly::FindAllCommitsRequest.new( repository: @gitaly_repo, diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index dfe60fb5a03..328f1f742c5 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -69,6 +69,10 @@ module Gitlab with_rate_limit { octokit.user(username) } end + def pull_request_reviews(repo_name, iid) + with_rate_limit { octokit.pull_request_reviews(repo_name, iid) } + end + # Returns the details of a GitHub repository. # # name - The path (in the form `owner/repository`) of the repository. @@ -76,6 +80,10 @@ module Gitlab with_rate_limit { octokit.repo(name) } end + def pull_request(repo_name, iid) + with_rate_limit { octokit.pull_request(repo_name, iid) } + end + def labels(*args) each_object(:labels, *args) end @@ -155,8 +163,8 @@ module Gitlab end end - def search_repos_by_name(name) - each_page(:search_repositories, search_query(str: name, type: :name)) + def search_repos_by_name(name, options = {}) + octokit.search_repositories(search_query(str: name, type: :name), options) end def search_query(str:, type:, include_collaborations: true, include_orgs: true) diff --git a/lib/gitlab/github_import/importer/lfs_objects_importer.rb b/lib/gitlab/github_import/importer/lfs_objects_importer.rb index 5980b3c2179..c74a7706117 100644 --- a/lib/gitlab/github_import/importer/lfs_objects_importer.rb +++ b/lib/gitlab/github_import/importer/lfs_objects_importer.rb @@ -23,16 +23,13 @@ module Gitlab end def each_object_to_import - lfs_objects = Projects::LfsPointers::LfsImportService.new(project).execute + lfs_objects = Projects::LfsPointers::LfsObjectDownloadListService.new(project).execute lfs_objects.each do |object| yield object end rescue StandardError => e - Gitlab::Import::Logger.error( - message: 'The Lfs import process failed', - error: e.message - ) + error(project.id, e) end end end diff --git a/lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb b/lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb new file mode 100644 index 00000000000..11181edf0e9 --- /dev/null +++ b/lib/gitlab/github_import/importer/pull_request_merged_by_importer.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Importer + class PullRequestMergedByImporter + def initialize(pull_request, project, client) + @project = project + @pull_request = pull_request + @client = client + end + + def execute + merge_request = project.merge_requests.find_by_iid(pull_request.iid) + user_finder = GithubImport::UserFinder.new(project, client) + gitlab_user_id = user_finder.user_id_for(pull_request.merged_by) + + if gitlab_user_id + timestamp = Time.new.utc + MergeRequest::Metrics.upsert({ + target_project_id: project.id, + merge_request_id: merge_request.id, + merged_by_id: gitlab_user_id, + created_at: timestamp, + updated_at: timestamp + }, unique_by: :merge_request_id) + else + merge_request.notes.create!( + importing: true, + note: "*Merged by: #{pull_request.merged_by.login}*", + author_id: project.creator_id, + project: project, + created_at: pull_request.created_at + ) + end + end + + private + + attr_reader :project, :pull_request, :client + end + end + end +end diff --git a/lib/gitlab/github_import/importer/pull_request_review_importer.rb b/lib/gitlab/github_import/importer/pull_request_review_importer.rb new file mode 100644 index 00000000000..14ee69ba089 --- /dev/null +++ b/lib/gitlab/github_import/importer/pull_request_review_importer.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Importer + class PullRequestReviewImporter + def initialize(review, project, client) + @review = review + @project = project + @client = client + @merge_request = project.merge_requests.find_by_id(review.merge_request_id) + end + + def execute + user_finder = GithubImport::UserFinder.new(project, client) + gitlab_user_id = user_finder.user_id_for(review.author) + + if gitlab_user_id + add_review_note!(gitlab_user_id) + add_approval!(gitlab_user_id) + else + add_complementary_review_note!(project.creator_id) + end + end + + private + + attr_reader :review, :merge_request, :project, :client + + def add_review_note!(author_id) + return if review.note.empty? + + add_note!(author_id, review_note_content) + end + + def add_complementary_review_note!(author_id) + return if review.note.empty? && !review.approval? + + note = "*Created by %{login}*\n\n%{note}" % { + note: review_note_content, + login: review.author.login + } + + add_note!(author_id, note) + end + + def review_note_content + header = "**Review:** #{review.review_type.humanize}" + + if review.note.present? + "#{header}\n\n#{review.note}" + else + header + end + end + + def add_note!(author_id, note) + note = Note.new(note_attributes(author_id, note)) + + note.save! + end + + def note_attributes(author_id, note, extra = {}) + { + importing: true, + noteable_id: merge_request.id, + noteable_type: 'MergeRequest', + project_id: project.id, + author_id: author_id, + note: note, + system: false, + created_at: review.submitted_at, + updated_at: review.submitted_at + }.merge(extra) + end + + def add_approval!(user_id) + return unless review.review_type == 'APPROVED' + + add_approval_system_note!(user_id) + + merge_request.approvals.create!( + user_id: user_id, + created_at: review.submitted_at + ) + end + + def add_approval_system_note!(user_id) + attributes = note_attributes( + user_id, + 'approved this merge request', + system: true, + system_note_metadata: SystemNoteMetadata.new(action: 'approved') + ) + + Note.create!(attributes) + end + end + end + end +end diff --git a/lib/gitlab/github_import/importer/pull_requests_importer.rb b/lib/gitlab/github_import/importer/pull_requests_importer.rb index dcae8ca01fa..7f1569f592f 100644 --- a/lib/gitlab/github_import/importer/pull_requests_importer.rb +++ b/lib/gitlab/github_import/importer/pull_requests_importer.rb @@ -11,7 +11,7 @@ module Gitlab end def representation_class - Representation::PullRequest + Gitlab::GithubImport::Representation::PullRequest end def sidekiq_worker_class diff --git a/lib/gitlab/github_import/importer/pull_requests_merged_by_importer.rb b/lib/gitlab/github_import/importer/pull_requests_merged_by_importer.rb new file mode 100644 index 00000000000..466288fde4c --- /dev/null +++ b/lib/gitlab/github_import/importer/pull_requests_merged_by_importer.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Importer + class PullRequestsMergedByImporter + include ParallelScheduling + + def importer_class + PullRequestMergedByImporter + end + + def representation_class + Gitlab::GithubImport::Representation::PullRequest + end + + def sidekiq_worker_class + ImportPullRequestMergedByWorker + end + + def collection_method + :pull_requests_merged_by + end + + def id_for_already_imported_cache(pr) + pr.number + end + + def each_object_to_import + project.merge_requests.with_state(:merged).find_each do |merge_request| + pull_request = client.pull_request(project.import_source, merge_request.iid) + yield(pull_request) + end + end + end + end + end +end diff --git a/lib/gitlab/github_import/importer/pull_requests_reviews_importer.rb b/lib/gitlab/github_import/importer/pull_requests_reviews_importer.rb new file mode 100644 index 00000000000..6d1b588f0e0 --- /dev/null +++ b/lib/gitlab/github_import/importer/pull_requests_reviews_importer.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Importer + class PullRequestsReviewsImporter + include ParallelScheduling + + def importer_class + PullRequestReviewImporter + end + + def representation_class + Gitlab::GithubImport::Representation::PullRequestReview + end + + def sidekiq_worker_class + ImportPullRequestReviewWorker + end + + def collection_method + :pull_request_reviews + end + + def id_for_already_imported_cache(review) + review.github_id + end + + def each_object_to_import + project.merge_requests.find_each do |merge_request| + reviews = client.pull_request_reviews(project.import_source, merge_request.iid) + reviews.each do |review| + review.merge_request_id = merge_request.id + yield(review) + end + end + end + end + end + end +end diff --git a/lib/gitlab/github_import/parallel_scheduling.rb b/lib/gitlab/github_import/parallel_scheduling.rb index cabc615ea11..51859010ec3 100644 --- a/lib/gitlab/github_import/parallel_scheduling.rb +++ b/lib/gitlab/github_import/parallel_scheduling.rb @@ -26,6 +26,8 @@ module Gitlab end def execute + info(project.id, message: "starting importer") + retval = if parallel? parallel_import @@ -43,8 +45,13 @@ module Gitlab # completed those jobs will just cycle through any remaining pages while # not scheduling anything. Gitlab::Cache::Import::Caching.expire(already_imported_cache_key, 15.minutes.to_i) + info(project.id, message: "importer finished") retval + rescue => e + error(project.id, e) + + raise e end # Imports all the objects in sequence in the current thread. @@ -157,6 +164,40 @@ module Gitlab def collection_options {} end + + private + + def info(project_id, extra = {}) + logger.info(log_attributes(project_id, extra)) + end + + def error(project_id, exception) + logger.error( + log_attributes( + project_id, + message: 'importer failed', + 'error.message': exception.message + ) + ) + + Gitlab::ErrorTracking.track_exception( + exception, + log_attributes(project_id) + ) + end + + def log_attributes(project_id, extra = {}) + extra.merge( + import_source: :github, + project_id: project_id, + importer: importer_class.name, + parallel: parallel? + ) + end + + def logger + @logger ||= Gitlab::Import::Logger.build + end end end end diff --git a/lib/gitlab/github_import/representation/pull_request.rb b/lib/gitlab/github_import/representation/pull_request.rb index 0ccc4bfaed3..be192762e05 100644 --- a/lib/gitlab/github_import/representation/pull_request.rb +++ b/lib/gitlab/github_import/representation/pull_request.rb @@ -13,18 +13,16 @@ module Gitlab :source_branch_sha, :target_branch, :target_branch_sha, :milestone_number, :author, :assignee, :created_at, :updated_at, :merged_at, :source_repository_id, - :target_repository_id, :source_repository_owner + :target_repository_id, :source_repository_owner, :merged_by # Builds a PR from a GitHub API response. # # issue - An instance of `Sawyer::Resource` containing the PR details. def self.from_api_response(pr) - assignee = - if pr.assignee - Representation::User.from_api_response(pr.assignee) - end - + assignee = Representation::User.from_api_response(pr.assignee) if pr.assignee user = Representation::User.from_api_response(pr.user) if pr.user + merged_by = Representation::User.from_api_response(pr.merged_by) if pr.merged_by + hash = { iid: pr.number, title: pr.title, @@ -42,7 +40,8 @@ module Gitlab assignee: assignee, created_at: pr.created_at, updated_at: pr.updated_at, - merged_at: pr.merged_at + merged_at: pr.merged_at, + merged_by: merged_by } new(hash) @@ -57,8 +56,8 @@ module Gitlab # Assignees are optional so we only convert it from a Hash if one was # set. - hash[:assignee] &&= Representation::User - .from_json_hash(hash[:assignee]) + hash[:assignee] &&= Representation::User.from_json_hash(hash[:assignee]) + hash[:merged_by] &&= Representation::User.from_json_hash(hash[:merged_by]) new(hash) end diff --git a/lib/gitlab/github_import/representation/pull_request_review.rb b/lib/gitlab/github_import/representation/pull_request_review.rb new file mode 100644 index 00000000000..3205259a1ed --- /dev/null +++ b/lib/gitlab/github_import/representation/pull_request_review.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Representation + class PullRequestReview + include ToHash + include ExposeAttribute + + attr_reader :attributes + + expose_attribute :author, :note, :review_type, :submitted_at, :github_id, :merge_request_id + + def self.from_api_response(review) + user = Representation::User.from_api_response(review.user) if review.user + + new( + merge_request_id: review.merge_request_id, + author: user, + note: review.body, + review_type: review.state, + submitted_at: review.submitted_at, + github_id: review.id + ) + end + + # Builds a new note using a Hash that was built from a JSON payload. + def self.from_json_hash(raw_hash) + hash = Representation.symbolize_hash(raw_hash) + + hash[:author] &&= Representation::User.from_json_hash(hash[:author]) + hash[:submitted_at] = Time.parse(hash[:submitted_at]).in_time_zone + + new(hash) + end + + # attributes - A Hash containing the raw note details. The keys of this + # Hash must be Symbols. + def initialize(attributes) + @attributes = attributes + end + + def approval? + review_type == 'APPROVED' + end + end + end + end +end diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb index 352a93817be..d123989ef8e 100644 --- a/lib/gitlab/gl_repository.rb +++ b/lib/gitlab/gl_repository.rb @@ -20,6 +20,7 @@ module Gitlab end, container_class: ProjectWiki, project_resolver: -> (wiki) { wiki.try(:project) }, + guest_read_ability: :download_wiki_code, suffix: :wiki ).freeze SNIPPET = RepoType.new( diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 2d41ad76618..362da8ea53e 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -61,15 +61,15 @@ module Gitlab def push_frontend_feature_flag(name, *args, **kwargs) enabled = Feature.enabled?(name, *args, **kwargs) - push_to_gon_features(name, enabled) + push_to_gon_attributes(:features, name, enabled) end - def push_to_gon_features(name, enabled) + def push_to_gon_attributes(key, name, enabled) var_name = name.to_s.camelize(:lower) # Here the `true` argument signals gon that the value should be merged # into any existing ones, instead of overwriting them. This allows you to # use this method to push multiple feature flags. - gon.push({ features: { var_name => enabled } }, true) + gon.push({ key => { var_name => enabled } }, true) end def default_avatar_url @@ -83,3 +83,5 @@ module Gitlab end end end + +Gitlab::GonHelper.prepend_if_ee('EE::Gitlab::GonHelper') diff --git a/lib/gitlab/google_code_import/client.rb b/lib/gitlab/google_code_import/client.rb deleted file mode 100644 index 52d714880b5..00000000000 --- a/lib/gitlab/google_code_import/client.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module GoogleCodeImport - class Client - attr_reader :raw_data - - def self.mask_email(author) - parts = author.split("@", 2) - parts[0] = "#{parts[0][0...-3]}..." - parts.join("@") - end - - def initialize(raw_data) - @raw_data = raw_data - end - - def valid? - raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#user" && raw_data.key?("projects") - end - - def repos - @repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.select(&:git?) - end - - def incompatible_repos - @incompatible_repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.reject(&:git?) - end - - def repo(id) - repos.find { |repo| repo.id == id } - end - - def user_map - user_map = Hash.new { |hash, user| hash[user] = self.class.mask_email(user) } - - repos.each do |repo| - next unless repo.valid? && repo.issues - - repo.issues.each do |raw_issue| - # Touching is enough to add the entry and masked email. - user_map[raw_issue["author"]["name"]] - - raw_issue["comments"]["items"].each do |raw_comment| - user_map[raw_comment["author"]["name"]] - end - end - end - - Hash[user_map.sort] - end - end - end -end diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb deleted file mode 100644 index 4da2004b74f..00000000000 --- a/lib/gitlab/google_code_import/importer.rb +++ /dev/null @@ -1,373 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module GoogleCodeImport - class Importer - attr_reader :project, :repo, :closed_statuses - - NICE_LABEL_COLOR_HASH = - { - 'Status: New' => '#428bca', - 'Status: Accepted' => '#5cb85c', - 'Status: Started' => '#8e44ad', - 'Priority: Critical' => '#ffcfcf', - 'Priority: High' => '#deffcf', - 'Priority: Medium' => '#fff5cc', - 'Priority: Low' => '#cfe9ff', - 'Type: Defect' => '#d9534f', - 'Type: Enhancement' => '#44ad8e', - 'Type: Task' => '#4b6dd0', - 'Type: Review' => '#8e44ad', - 'Type: Other' => '#7f8c8d' - }.freeze - - def initialize(project) - @project = project - - import_data = project.import_data.try(:data) - repo_data = import_data["repo"] if import_data - @repo = GoogleCodeImport::Repository.new(repo_data) - - @closed_statuses = [] - @known_labels = Set.new - end - - def execute - return true unless repo.valid? - - import_status_labels - - import_labels - - import_issues - - true - end - - private - - def user_map - @user_map ||= begin - user_map = Hash.new do |hash, user| - # Replace ... by \.\.\., so `johnsm...@gmail.com` isn't autolinked. - Client.mask_email(user).sub("...", "\\.\\.\\.") - end - - import_data = project.import_data.try(:data) - stored_user_map = import_data["user_map"] if import_data - user_map.update(stored_user_map) if stored_user_map - - user_map - end - end - - def import_status_labels - repo.raw_data["issuesConfig"]["statuses"].each do |status| - closed = !status["meansOpen"] - @closed_statuses << status["status"] if closed - - name = nice_status_name(status["status"]) - create_label(name) - @known_labels << name - end - end - - def import_labels - repo.raw_data["issuesConfig"]["labels"].each do |label| - name = nice_label_name(label["label"]) - create_label(name) - @known_labels << name - end - end - - # rubocop: disable CodeReuse/ActiveRecord - def import_issues - return unless repo.issues - - while raw_issue = repo.issues.shift - author = user_map[raw_issue["author"]["name"]] - date = DateTime.parse(raw_issue["published"]).to_formatted_s(:long) - - comments = raw_issue["comments"]["items"] - issue_comment = comments.shift - - content = format_content(issue_comment["content"]) - attachments = format_attachments(raw_issue["id"], 0, issue_comment["attachments"]) - - body = format_issue_body(author, date, content, attachments) - labels = import_issue_labels(raw_issue) - - assignee_id = nil - if raw_issue.key?("owner") - username = user_map[raw_issue["owner"]["name"]] - - if username.start_with?("@") - username = username[1..-1] - - if user = UserFinder.new(username).find_by_username - assignee_id = user.id - end - end - end - - issue = Issue.create!( - iid: raw_issue['id'], - project_id: project.id, - title: raw_issue['title'], - description: body, - author_id: project.creator_id, - assignee_ids: [assignee_id], - state_id: raw_issue['state'] == 'closed' ? Issue.available_states[:closed] : Issue.available_states[:opened] - ) - - issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true) - issue.update_attribute(:label_ids, issue_labels.pluck(:id)) - - import_issue_comments(issue, comments) - end - end - # rubocop: enable CodeReuse/ActiveRecord - - def import_issue_labels(raw_issue) - labels = [] - - raw_issue["labels"].each do |label| - name = nice_label_name(label) - labels << name - - unless @known_labels.include?(name) - create_label(name) - @known_labels << name - end - end - - labels << nice_status_name(raw_issue["status"]) - labels - end - - def import_issue_comments(issue, comments) - Note.transaction do - while raw_comment = comments.shift - next if raw_comment.key?("deletedBy") - - content = format_content(raw_comment["content"]) - updates = format_updates(raw_comment["updates"]) - attachments = format_attachments(issue.iid, raw_comment["id"], raw_comment["attachments"]) - - next if content.blank? && updates.blank? && attachments.blank? - - author = user_map[raw_comment["author"]["name"]] - date = DateTime.parse(raw_comment["published"]).to_formatted_s(:long) - - body = format_issue_comment_body( - raw_comment["id"], - author, - date, - content, - updates, - attachments - ) - - # Needs to match order of `comment_columns` below. - Note.create!( - project_id: project.id, - noteable_type: "Issue", - noteable_id: issue.id, - author_id: project.creator_id, - note: body - ) - end - end - end - - def nice_label_color(name) - NICE_LABEL_COLOR_HASH[name] || - case name - when /\AComponent:/ - '#fff39e' - when /\AOpSys:/ - '#e2e2e2' - when /\AMilestone:/ - '#fee3ff' - when *closed_statuses.map { |s| nice_status_name(s) } - '#cfcfcf' - else - '#e2e2e2' - end - end - - def nice_label_name(name) - name.sub("-", ": ") - end - - def nice_status_name(name) - "Status: #{name}" - end - - def linkify_issues(str) - str = str.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2') - str = str.gsub(/([Cc]omment) #([0-9]+)/, '\1 \2') - str - end - - def escape_for_markdown(str) - # No headings and lists - str = str.gsub(/^#/, "\\#") - str = str.gsub(/^-/, "\\-") - - # No inline code - str = str.gsub("`", "\\`") - - # Carriage returns make me sad - str = str.delete("\r") - - # Markdown ignores single newlines, but we need them as <br />. - str = str.gsub("\n", " \n") - - str - end - - def create_label(name) - params = { name: name, color: nice_label_color(name) } - ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true) - end - - def format_content(raw_content) - linkify_issues(escape_for_markdown(raw_content)) - end - - def format_updates(raw_updates) - updates = [] - - if raw_updates.key?("status") - updates << "*Status: #{raw_updates["status"]}*" - end - - if raw_updates.key?("owner") - updates << "*Owner: #{user_map[raw_updates["owner"]]}*" - end - - if raw_updates.key?("cc") - cc = raw_updates["cc"].map do |l| - deleted = l.start_with?("-") - l = l[1..-1] if deleted - l = user_map[l] - l = "~~#{l}~~" if deleted - l - end - - updates << "*Cc: #{cc.join(", ")}*" - end - - if raw_updates.key?("labels") - labels = raw_updates["labels"].map do |l| - deleted = l.start_with?("-") - l = l[1..-1] if deleted - l = nice_label_name(l) - l = "~~#{l}~~" if deleted - l - end - - updates << "*Labels: #{labels.join(", ")}*" - end - - if raw_updates.key?("mergedInto") - updates << "*Merged into: ##{raw_updates["mergedInto"]}*" - end - - if raw_updates.key?("blockedOn") - blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on| - format_blocking_updates(raw_blocked_on) - end - - updates << "*Blocked on: #{blocked_ons.join(", ")}*" - end - - if raw_updates.key?("blocking") - blockings = raw_updates["blocking"].map do |raw_blocked_on| - format_blocking_updates(raw_blocked_on) - end - - updates << "*Blocking: #{blockings.join(", ")}*" - end - - updates - end - - def format_blocking_updates(raw_blocked_on) - name, id = raw_blocked_on.split(":", 2) - - deleted = name.start_with?("-") - name = name[1..-1] if deleted - - text = - if name == project.import_source - "##{id}" - else - "#{project.namespace.full_path}/#{name}##{id}" - end - - text = "~~#{text}~~" if deleted - text - end - - def format_attachments(issue_id, comment_id, raw_attachments) - return [] unless raw_attachments - - raw_attachments.map do |attachment| - next if attachment["isDeleted"] - - filename = attachment["fileName"] - link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{filename}" - - text = "[#{filename}](#{link})" - text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/i - text - end.compact - end - - def format_issue_comment_body(id, author, date, content, updates, attachments) - body = [] - body << "*Comment #{id} by #{author} on #{date}*" - body << "---" - - if content.blank? - content = "*(No comment has been entered for this change)*" - end - - body << content - - if updates.any? - body << "---" - body += updates - end - - if attachments.any? - body << "---" - body += attachments - end - - body.join("\n\n") - end - - def format_issue_body(author, date, content, attachments) - body = [] - body << "*By #{author} on #{date} (imported from Google Code)*" - body << "---" - - if content.blank? - content = "*(No description has been entered for this issue)*" - end - - body << content - - if attachments.any? - body << "---" - body += attachments - end - - body.join("\n\n") - end - end - end -end diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb deleted file mode 100644 index eaef85acb98..00000000000 --- a/lib/gitlab/google_code_import/project_creator.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module GoogleCodeImport - class ProjectCreator - attr_reader :repo, :namespace, :current_user, :user_map - - def initialize(repo, namespace, current_user, user_map = nil) - @repo = repo - @namespace = namespace - @current_user = current_user - @user_map = user_map - end - - def execute - ::Projects::CreateService.new( - current_user, - name: repo.name, - path: repo.name, - description: repo.summary, - namespace: namespace, - creator: current_user, - visibility_level: Gitlab::VisibilityLevel::PUBLIC, - import_type: "google_code", - import_source: repo.name, - import_url: repo.import_url, - import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map } } - ).execute - end - end - end -end diff --git a/lib/gitlab/google_code_import/repository.rb b/lib/gitlab/google_code_import/repository.rb deleted file mode 100644 index 19627c8cd35..00000000000 --- a/lib/gitlab/google_code_import/repository.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module GoogleCodeImport - class Repository - attr_accessor :raw_data - - def initialize(raw_data) - @raw_data = raw_data - end - - def valid? - raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#project" - end - - def id - raw_data["externalId"] - end - - def name - raw_data["name"] - end - - def summary - raw_data["summary"] - end - - def description - raw_data["description"] - end - - def git? - raw_data["versionControlSystem"] == "git" - end - - def import_url - raw_data["repositoryUrls"].first - end - - def issues - raw_data["issues"] && raw_data["issues"]["items"] - end - end - end -end diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 8166bef4510..b1494cf8cf2 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -142,13 +142,11 @@ module Gitlab end def tmp_keychains_created - @tmp_keychains_created ||= Gitlab::Metrics.counter(:gpg_tmp_keychains_created_total, - 'The number of temporary GPG keychains created') + Gitlab::Metrics.counter(:gpg_tmp_keychains_created_total, 'The number of temporary GPG keychains created') end def tmp_keychains_removed - @tmp_keychains_removed ||= Gitlab::Metrics.counter(:gpg_tmp_keychains_removed_total, - 'The number of temporary GPG keychains removed') + Gitlab::Metrics.counter(:gpg_tmp_keychains_removed_total, 'The number of temporary GPG keychains removed') end end end diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb index c70127553fd..6ee446011d4 100644 --- a/lib/gitlab/graphql/authorize/authorize_resource.rb +++ b/lib/gitlab/graphql/authorize/authorize_resource.rb @@ -62,8 +62,8 @@ module Gitlab end end - def raise_resource_not_available_error! - raise Gitlab::Graphql::Errors::ResourceNotAvailable, RESOURCE_ACCESS_ERROR + def raise_resource_not_available_error!(msg = RESOURCE_ACCESS_ERROR) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, msg end end end diff --git a/lib/gitlab/graphql/connection_collection_methods.rb b/lib/gitlab/graphql/connection_collection_methods.rb new file mode 100644 index 00000000000..0e2c4a98bb6 --- /dev/null +++ b/lib/gitlab/graphql/connection_collection_methods.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module ConnectionCollectionMethods + extend ActiveSupport::Concern + + included do + delegate :to_a, :size, :include?, :empty?, to: :nodes + end + end + end +end diff --git a/lib/gitlab/graphql/connection_redaction.rb b/lib/gitlab/graphql/connection_redaction.rb new file mode 100644 index 00000000000..5e037bb9f63 --- /dev/null +++ b/lib/gitlab/graphql/connection_redaction.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module ConnectionRedaction + class RedactionState + attr_reader :redactor + attr_reader :redacted_nodes + + def redactor=(redactor) + @redactor = redactor + @redacted_nodes = nil + end + + def redacted(&block) + @redacted_nodes ||= redactor.present? ? redactor.redact(yield) : yield + end + end + + delegate :redactor=, to: :redaction_state + + def nodes + redaction_state.redacted { super.to_a } + end + + private + + def redaction_state + @redaction_state ||= RedactionState.new + end + end + end +end diff --git a/lib/gitlab/graphql/deferred.rb b/lib/gitlab/graphql/deferred.rb new file mode 100644 index 00000000000..d0b36aabd5f --- /dev/null +++ b/lib/gitlab/graphql/deferred.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# A marker interface that allows use to lazily resolve a wider range of value +module Gitlab + module Graphql + module Deferred + def execute + raise NotImplementedError, 'Deferred classes must provide an execute method' + end + end + end +end diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb index 503b1064b11..ad9e08e189c 100644 --- a/lib/gitlab/graphql/docs/helper.rb +++ b/lib/gitlab/graphql/docs/helper.rb @@ -13,6 +13,12 @@ module Gitlab def auto_generated_comment <<-MD.strip_heredoc + --- + stage: Plan + group: Project Management + info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers + --- + <!--- This documentation is auto generated by a script. diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml index 97df4233905..8f5a1788fa5 100644 --- a/lib/gitlab/graphql/docs/templates/default.md.haml +++ b/lib/gitlab/graphql/docs/templates/default.md.haml @@ -12,7 +12,7 @@ Each table below documents a GraphQL type. Types match loosely to models, but not all fields and methods on a model are available via GraphQL. - CAUTION: **Caution:** + WARNING: Fields that are deprecated are marked with **{warning-solid}**. Items (fields, enums, etc) that have been removed according to our [deprecation process](../index.md#deprecation-process) can be found in [Removed Items](../removed_items.md). @@ -21,7 +21,7 @@ :plain ## Object types - Object types represent the resources that GitLab's GraphQL API can return. + Object types represent the resources that the GitLab GraphQL API can return. They contain _fields_. Each field has its own type, which will either be one of the basic GraphQL [scalar types](https://graphql.org/learn/schema/#scalar-types) (e.g.: `String` or `Boolean`) or other object types. diff --git a/lib/gitlab/graphql/expose_permissions.rb b/lib/gitlab/graphql/expose_permissions.rb index 365b7cca24f..ab9ed354673 100644 --- a/lib/gitlab/graphql/expose_permissions.rb +++ b/lib/gitlab/graphql/expose_permissions.rb @@ -9,7 +9,7 @@ module Gitlab field :user_permissions, permission_type, description: description, null: false, - resolve: -> (obj, _, _) { obj } + method: :itself end end end diff --git a/lib/gitlab/graphql/externally_paginated_array.rb b/lib/gitlab/graphql/externally_paginated_array.rb index 4797fe15cd3..873d7f4efdf 100644 --- a/lib/gitlab/graphql/externally_paginated_array.rb +++ b/lib/gitlab/graphql/externally_paginated_array.rb @@ -3,12 +3,12 @@ module Gitlab module Graphql class ExternallyPaginatedArray < Array - attr_reader :previous_cursor, :next_cursor + attr_reader :start_cursor, :end_cursor def initialize(previous_cursor, next_cursor, *args) super(args) - @previous_cursor = previous_cursor - @next_cursor = next_cursor + @start_cursor = previous_cursor + @end_cursor = next_cursor end end end diff --git a/lib/gitlab/graphql/laziness.rb b/lib/gitlab/graphql/laziness.rb new file mode 100644 index 00000000000..749d832919d --- /dev/null +++ b/lib/gitlab/graphql/laziness.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + # This module allows your class to easily defer and force values. + # Its methods are just sugar for calls to the Gitlab::Graphql::Lazy class. + # + # example: + # + # class MyAwesomeClass + # include ::Gitlab::Graphql::Laziness + # + # # takes a list of id and list of factors, and computes + # # sum of [SomeObject[i]#value * factor[i]] + # def resolve(ids:, factors:) + # ids.zip(factors) + # .map { |id, factor| promise_an_int(id, factor) } + # .map(&method(:force)) + # .sum + # end + # + # # returns a promise for an Integer + # def (id, factor) + # thunk = SomeObject.lazy_find(id) + # defer { force(thunk).value * factor } + # end + # end + # + # In the example above, we use defer to delay forcing the batch-loaded + # item until we need it, and then we use `force` to consume the lazy values + # + # If `SomeObject.lazy_find(id)` batches correctly, calling + # `resolve` will only perform one batched load for all objects, rather than + # loading them individually before combining the results. + # + module Laziness + def defer(&block) + ::Gitlab::Graphql::Lazy.new(&block) + end + + def force(lazy) + ::Gitlab::Graphql::Lazy.force(lazy) + end + end + end +end diff --git a/lib/gitlab/graphql/lazy.rb b/lib/gitlab/graphql/lazy.rb index 3cc11047387..54013cf4790 100644 --- a/lib/gitlab/graphql/lazy.rb +++ b/lib/gitlab/graphql/lazy.rb @@ -24,6 +24,8 @@ module Gitlab value.force when ::BatchLoader::GraphQL value.sync + when ::Gitlab::Graphql::Deferred + value.execute when ::GraphQL::Execution::Lazy value.value # part of the private api, but we can force this as well when ::Concurrent::Promise diff --git a/lib/gitlab/graphql/pagination/array_connection.rb b/lib/gitlab/graphql/pagination/array_connection.rb new file mode 100644 index 00000000000..efc912eaeca --- /dev/null +++ b/lib/gitlab/graphql/pagination/array_connection.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# We use the Keyset / Stable cursor connection by default for ActiveRecord::Relation. +# However, there are times when that may not be powerful enough (yet), and we +# want to use standard offset pagination. +module Gitlab + module Graphql + module Pagination + class ArrayConnection < ::GraphQL::Pagination::ArrayConnection + prepend ::Gitlab::Graphql::ConnectionRedaction + include ::Gitlab::Graphql::ConnectionCollectionMethods + end + end + end +end diff --git a/lib/gitlab/graphql/pagination/connections.rb b/lib/gitlab/graphql/pagination/connections.rb index 8f37fa3f474..54a84be4274 100644 --- a/lib/gitlab/graphql/pagination/connections.rb +++ b/lib/gitlab/graphql/pagination/connections.rb @@ -12,6 +12,10 @@ module Gitlab schema.connections.add( Gitlab::Graphql::ExternallyPaginatedArray, Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection) + + schema.connections.add( + Array, + Gitlab::Graphql::Pagination::ArrayConnection) end end end diff --git a/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb b/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb index 12e047420bf..ce309df65d9 100644 --- a/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb +++ b/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb @@ -5,13 +5,10 @@ module Gitlab module Graphql module Pagination class ExternallyPaginatedArrayConnection < GraphQL::Pagination::ArrayConnection - def start_cursor - items.previous_cursor - end + include ::Gitlab::Graphql::ConnectionCollectionMethods + prepend ::Gitlab::Graphql::ConnectionRedaction - def end_cursor - items.next_cursor - end + delegate :start_cursor, :end_cursor, to: :items def next_page? end_cursor.present? diff --git a/lib/gitlab/graphql/pagination/keyset/connection.rb b/lib/gitlab/graphql/pagination/keyset/connection.rb index 252f6371765..2ad8d2f7ab7 100644 --- a/lib/gitlab/graphql/pagination/keyset/connection.rb +++ b/lib/gitlab/graphql/pagination/keyset/connection.rb @@ -31,6 +31,8 @@ module Gitlab module Keyset class Connection < GraphQL::Pagination::ActiveRecordRelationConnection include Gitlab::Utils::StrongMemoize + include ::Gitlab::Graphql::ConnectionCollectionMethods + prepend ::Gitlab::Graphql::ConnectionRedaction # rubocop: disable Naming/PredicateName # https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.Fields diff --git a/lib/gitlab/graphql/pagination/keyset/order_info.rb b/lib/gitlab/graphql/pagination/keyset/order_info.rb index f3ce3a10703..d37264c1343 100644 --- a/lib/gitlab/graphql/pagination/keyset/order_info.rb +++ b/lib/gitlab/graphql/pagination/keyset/order_info.rb @@ -127,3 +127,5 @@ module Gitlab end end end + +Gitlab::Graphql::Pagination::Keyset::OrderInfo.prepend_if_ee('EE::Gitlab::Graphql::Pagination::Keyset::OrderInfo') diff --git a/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb b/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb index 33f84701562..4a57b7aceca 100644 --- a/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb +++ b/lib/gitlab/graphql/pagination/offset_active_record_relation_connection.rb @@ -7,6 +7,8 @@ module Gitlab module Graphql module Pagination class OffsetActiveRecordRelationConnection < GraphQL::Pagination::ActiveRecordRelationConnection + prepend ::Gitlab::Graphql::ConnectionRedaction + include ::Gitlab::Graphql::ConnectionCollectionMethods end end end diff --git a/lib/gitlab/hook_data/base_builder.rb b/lib/gitlab/hook_data/base_builder.rb index d54175bce81..434d30d9717 100644 --- a/lib/gitlab/hook_data/base_builder.rb +++ b/lib/gitlab/hook_data/base_builder.rb @@ -21,6 +21,13 @@ module Gitlab private + def timestamps_data + { + created_at: object.created_at&.xmlschema, + updated_at: object.updated_at&.xmlschema + } + end + def absolute_image_urls(markdown_text) return markdown_text unless markdown_text.present? diff --git a/lib/gitlab/hook_data/group_member_builder.rb b/lib/gitlab/hook_data/group_member_builder.rb new file mode 100644 index 00000000000..32cfd032ffe --- /dev/null +++ b/lib/gitlab/hook_data/group_member_builder.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Gitlab + module HookData + class GroupMemberBuilder < BaseBuilder + alias_method :group_member, :object + + # Sample data + + # { + # :event_name=>"user_add_to_group", + # :group_name=>"GitLab group", + # :group_path=>"gitlab", + # :group_id=>1, + # :user_username=>"robert", + # :user_name=>"Robert Mills", + # :user_email=>"robert@example.com", + # :user_id=>14, + # :group_access=>"Guest", + # :created_at=>"2020-11-04T10:12:10Z", + # :updated_at=>"2020-11-04T10:12:10Z", + # :expires_at=>"2020-12-04T10:12:10Z" + # } + + def build(event) + [ + timestamps_data, + group_member_data, + event_data(event) + ].reduce(:merge) + end + + private + + def group_member_data + { + group_name: group_member.group.name, + group_path: group_member.group.path, + group_id: group_member.group.id, + user_username: group_member.user.username, + user_name: group_member.user.name, + user_email: group_member.user.email, + user_id: group_member.user.id, + group_access: group_member.human_access, + expires_at: group_member.expires_at&.xmlschema + } + end + + def event_data(event) + event_name = case event + when :create + 'user_add_to_group' + when :destroy + 'user_remove_from_group' + when :update + 'user_update_for_group' + end + + { event_name: event_name } + end + end + end +end + +Gitlab::HookData::GroupMemberBuilder.prepend_if_ee('EE::Gitlab::HookData::GroupMemberBuilder') diff --git a/lib/gitlab/i18n/html_todo.yml b/lib/gitlab/i18n/html_todo.yml deleted file mode 100644 index 91e01f8a0b8..00000000000 --- a/lib/gitlab/i18n/html_todo.yml +++ /dev/null @@ -1,314 +0,0 @@ -# -# PLEASE DO NOT ADD NEW STRINGS TO THIS FILE. -# -# See https://docs.gitlab.com/ee/development/i18n/externalization.html#html -# for information on how to handle HTML in translations. - -# -# This file contains strings that need to be fixed to use the -# updated HTML guidelines. Any strings in this file will no -# longer be able to be translated until they have been updated. -# -# This file (and the functionality around it) will be removed -# once https://gitlab.com/gitlab-org/gitlab/-/issues/217933 is complete. -# -# See https://gitlab.com/gitlab-org/gitlab/-/issues/19485 for more details -# why this change has been made. -# - -# -# Strings below are fixed in the source code but the translations are still present in CrowdIn so the -# locale files will fail the linter. They can be deleted after next CrowdIn sync, likely in: -# https://gitlab.com/gitlab-org/gitlab/-/issues/226008 -# - -"This commit was signed with an <strong>unverified</strong> signature.": - plural_id: - translations: - - "このコミットは<strong>検証されていない</strong> 署名でサインされています。" - - "Этот коммит был подписан <strong>непроверенной</strong> подписью." - - "此提交使用 <strong>未经验证的</strong> 签名进行签名。" - - "Цей коміт підписано <strong>неперевіреним</strong> підписом." - - "Esta commit fue firmado con una firma <strong>no verificada</strong>." -"This commit was signed with a <strong>verified</strong> signature and the committer email is verified to belong to the same user.": - plural_id: - translations: - - "このコミットは <strong>検証済み</strong> の署名でサインされており、このコミッターのメールは同じユーザーのものであることが検証されています。" - - "Это коммит был подписан <strong>верифицированной</strong> подписью и коммитер подтвердил, что адрес почты принадлежит ему." - - "此提交使用 <strong>已验证</strong> 的签名进行签名,并且已验证提交者的电子邮件属于同一用户。" - - "Цей коміт підписано <strong>перевіреним</strong> підписом і адреса електронної пошти комітера гарантовано належить тому самому користувачу." - - "Este commit fue firmado con una firma verificada, y <strong>se ha verificado</strong> que la dirección de correo electrónico del committer y la firma pertenecen al mismo usuario." -"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}": - plural_id: - translations: - - "分支 <strong>%{branch_name}</strong> 已創建。如需設置自動部署, 請選擇合適的 GitLab CI Yaml 模板併提交更改。%{link_to_autodeploy_doc}" - - "O branch <strong>%{branch_name}</strong> foi criado. Para configurar o deploy automático, selecione um modelo de Yaml do GitLab CI e commit suas mudanças. %{link_to_autodeploy_doc}" - - "<strong>%{branch_name}</strong> ブランチが作成されました。自動デプロイを設定するには、GitLab CI Yaml テンプレートを選択して、変更をコミットしてください。 %{link_to_autodeploy_doc}" - - "La branch <strong>%{branch_name}</strong> è stata creata. Per impostare un rilascio automatico scegli un template CI di Gitlab e committa le tue modifiche %{link_to_autodeploy_doc}" - - "O ramo <strong>%{branch_name}</strong> foi criado. Para configurar a implantação automática, seleciona um modelo de Yaml do GitLab CI e envia as tuas alterações. %{link_to_autodeploy_doc}" - - "Ветка <strong>%{branch_name}</strong> создана. Для настройки автоматического развертывания выберите YAML-шаблон для GitLab CI и зафиксируйте свои изменения. %{link_to_autodeploy_doc}" - - "已创建分支 <strong>%{branch_name}</strong> 。如需设置自动部署, 请选择合适的 GitLab CI Yaml 模板并提交更改。%{link_to_autodeploy_doc}" - - "Гілка <strong>%{branch_name}</strong> створена. Для настройки автоматичного розгортання виберіть GitLab CI Yaml-шаблон і закомітьте зміни. %{link_to_autodeploy_doc}" - - "Клонът <strong>%{branch_name}</strong> беше създаден. За да настроите автоматичното внедряване, изберете Yaml шаблон за GitLab CI и подайте промените си. %{link_to_autodeploy_doc}" - - "Branch <strong>%{branch_name}</strong> wurde erstellt. Um die automatische Bereitstellung einzurichten, wähle eine GitLab CI Yaml Vorlage und committe deine Änderungen. %{link_to_autodeploy_doc}" - - "<strong>%{branch_name}</strong> 브랜치가 생성되었습니다. 자동 배포를 설정하려면 GitLab CI Yaml 템플릿을 선택하고 변경 사항을 적용하십시오. %{link_to_autodeploy_doc}" - - "La branĉo <strong>%{branch_name}</strong> estis kreita. Por agordi aŭtomatan disponigadon, bonvolu elekti Yaml-ŝablonon por GitLab CI kaj enmeti viajn ŝanĝojn. %{link_to_autodeploy_doc}" - - "La branche <strong>%{branch_name}</strong> a été créée. Pour mettre en place le déploiement automatisé, sélectionnez un modèle de fichier YAML pour l’intégration continue (CI) de GitLab, et validez les modifications. %{link_to_autodeploy_doc}" - - "La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}" -"GitLabPages|GitLab Pages are disabled for this project. You can enable them on your project's %{strong_start}Settings > General > Visibility%{strong_end} page.": - plural_id: - translations: - - "GitLab Pagesはこのプロジェクトでは無効になっています。 プロジェクトの%{strong_start} 設定> 全般> 可視性%{strong_end}ページで有効にできます。" - - "GitLab Pages отключены для этого проекта. Вы можете включить в поле %{strong_start}Настройки > Общие > Видимость%{strong_end} вашего проекта." - - "此项目禁用GitLab Pages。您可以在您的项目的%{strong_start}设置 > 常规 > 可见性%{strong_end} 页面启用。" - - "GitLab Pages вимкнено для цього проєкту. Ви можете їх увімкнути перейшовши на сторінку проєкту %{strong_start}Налаштування > Загальні > Видимість%{strong_end}." - - "Las páginas de GitLab están deshabilitadas para este proyecto. Puede habilitarlas en los ajustes %{strong_start} de su proyecto > General > Visibilidad%{strong_end}." -"You can invite a new member to <strong>%{project_name}</strong> or invite another group.": - plural_id: - translations: - - "新しいメンバーを<strong>%{project_name} </strong>に招待するか、別のグループを招待することができます。" - - "Podes convidar um novo para <strong>%{project_name}</strong> ou convidar outro grupo." - - "邀请新成员或另一个群组加入<strong>%{project_name}</strong>。" - - "Puede invitar a un nuevo miembro a <strong>%{project_name}</strong> o invitar a otro grupo." - - "<strong>%{project_name}</strong> projesine yeni bir üye davet edebilir veya başka bir grubu davet edebilirsiniz." - - "Вы можете пригласить нового участника в <strong>%{project_name}</strong> или пригласить другую группу." - - "Ви можете запросити нового учасника до <strong>%{project_name}</strong> або запросити іншу групу." -"You can invite a new member to <strong>%{project_name}</strong>.": - plural_id: - translations: - - "新しいメンバーを<strong>%{project_name} </strong>に招待できます。" - - "Podes convidar um novo membro para <strong>%{project_name}</strong>." - - "邀请新成员加入<strong>%{project_name}</strong>。" - - "Puedes invitar a un nuevo miembro a <strong>%{project_name}</strong>." - - "<strong>%{project_name}</strong> projesine yeni bir üye davet edebilirsiniz." - - "Вы можете пригласить нового участника в <strong>%{project_name}</strong>." - - "Ви можете запросити нового учасника до <strong>%{project_name}</strong>." -"You can invite another group to <strong>%{project_name}</strong>.": - plural_id: - translations: - - "他のグループを<strong>%{project_name} </strong>に招待できます。" - - "Podes convidar outro grupo para <strong>%{project_name}</strong>." - - "您可以邀请另一个群组加入<strong>%{project_name}</strong>。" - - "Ви можете запросити нову групу до <strong>%{project_name}</strong>." - - "Puedes invitar a otro grupo a <strong>%{project_name}</strong>." -"Example: <code>192.168.0.0/24</code>. %{read_more_link}.": - plural_id: - translations: -"Note that PostgreSQL %{pg_version_upcoming} will become the minimum required version in GitLab %{gl_version_upcoming} (%{gl_version_upcoming_date}). Please consider upgrading your environment to a supported PostgreSQL version soon, see <a href=\\\"%{pg_version_upcoming_url}\\\">the related epic</a> for details.": - plural_id: - translations: -"Authorize <strong>%{user}</strong> to use your account?": - plural_id: - translations: -"DeployFreeze|Specify times when deployments are not allowed for an environment. The <code>gitlab-ci.yml</code> file must be updated to make deployment jobs aware of the %{freeze_period_link_start}freeze period%{freeze_period_link_end}.": - plural_id: - translations: -"<project name>": - translations: - - "<название проекта>" - - "<project name>" - - "<proje adı>" - - "<naziv projekta>" - - "<ім’я проєкту>" - - "<프로젝트 이름>" -"<strong>Deletes</strong> source branch": - plural_id: - translations: - - "<strong>刪除</strong>來源分支" - - "<strong>Apagar</strong> branch de origem" - - "ソースブランチを<strong>削除</strong>" - - "<strong>刪除</strong>來源分支" - - "<strong>Apagar</strong> o ramo de origem" - - "<strong>Удаляет</strong> исходную ветку" - - "<strong>删除</strong>源分支" - - "<strong>Видаляє</strong> гілку-джерело" - - "<strong>Löscht</strong> den Quellbranch" - - "소스 브랜치 <strong>삭제</strong>" - - "<strong>Supprime</strong> la branche source" - - "<strong>elimina</strong> la rama origen" - - "Kaynak dalı <strong>siler</strong>" -"Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.": - plural_id: - translations: - - "Você está prestes a excluir este selo. Selos excluídos <strong>não podem</strong> ser restaurados." - - "このバッジを削除しようとしています。削除されたバッジは<strong>復元できません</strong>。" - - "Estás prestes a apagar este emblema. Emblemas apagados <strong>não podem</strong> ser restaurados." - - "Вы собираетесь удалить этот значок. Удаленные значки <strong>не могут</strong> быть восстановлены." - - "您即将删除此徽章。徽章被删除后 <strong>不能</strong> 恢复。" - - "Ви збираєтеся видалити цей значок. Вилучені значки <strong>не можуть</strong> бути відновлені." - - "Du bist gerade dabei dieses Badge zu entfernen. Entfernte Badges können <strong>nicht</strong> rückgängig gemacht werden." - - "이 배지를 삭제하려고합니다. 삭제 된 배지는 <strong>복원 할 수 없습니다</strong>." - - "Vous êtes sur le point de supprimer ce badge. Les badges supprimés <strong>ne peuvent pas</strong> être restaurés." - - "Va a eliminar esta insignia. Las insignias eliminadas <strong>no se pueden</strong> restaurar." - - "Bu rozeti sileceksiniz. Silinen rozetler geri <strong>yüklenemez</strong>." -"ClusterIntegration| This will permanently delete the following resources: <ul> <li>All installed applications and related resources</li> <li>The <code>gitlab-managed-apps</code> namespace</li> <li>Any project namespaces</li> <li><code>clusterroles</code></li> <li><code>clusterrolebindings</code></li> </ul>": - plural_id: - translations: - - "これにより、次のリソースは完全に削除されます <ul> <li>インストールされているすべてのアプリケーションと関連したリソース</li> <li> <code>gitlab-managed-apps</code> 名前空間</li> <li>任意のプロジェクト名前空間</li> <li><code>clusterroles</code></li> <li><code>clusterrolebindings</code></li> </ul>" - - "此操作将永久删除下列资源: <ul> <li>所有已安装的应用程序和相关资源</li> <li> <code>GitLab管理的应用</code> 命名空间</li> <li>任何项目命名空间</li> <li><code>clusterroles</code></li> <li><code>clusterrolebindings</code></li> </ul>" - - "Esto eliminará permanentemente los siguientes recursos: <ul> <li>Todas las aplicaciones instaladas y sus recursos relacionados</li> <li>El espacio de nombres <code>gitlab-managed-apps</code></li> <li>Cualquier espacio de nombres de proyecto</li> <li><code> clusterroles </code></li> <li><code>clusterrolebindings</code></li> </ul>" -"Configure a <code>.gitlab-webide.yml</code> file in the <code>.gitlab</code> directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}": - plural_id: - translations: - - "Configure um arquivo <code>.gitlab-webide.yml</code> no diretório <code>.gitlab</code> para começar a usar o Terminal Web. %{helpStart}Saiba mais.%{helpEnd}" - - "Webターミナルの使用を開始するには、 <code>.gitlab</code> ディレクトリの <code>.gitlab-webide.yml</code> ファイルを設定します。 詳細は%{helpStart}こちら%{helpEnd}です。" - - "Сконфигурируйте файл <code>.gitlab-webide.yml</code> в каталоге <code>.gitlab</code> чтобы начать использовать веб-терминал. %{helpStart}Узнайте больше.%{helpEnd}" - - "在 <code>.gitlab</code> 目录中配置 <code>.gitlab-webide.yml</code> 文件以开始使用Web终端。 %{helpStart}了解更多。%{helpEnd}" - - "Налаштуйте файл <code>.gitlab-webide.yml</code> у директорії <code>.gitlab</code>, щоб почати використовувати Веб-термінал. %{helpStart}Докладніше.%{helpEnd}" - - "웹 터미널 사용을 시작하도록 <code>.gitlab</code> 디렉토리에서 <code>.gitlab-webide.yml</code> 파일을 구성하십시오. %{helpStart}자세히 알아보십시오.%{helpEnd}" - - "Configure un archivo <code>.gitlab-webide.yml</code> en el directorio <code>.gitlab</code> para comenzar a utilizar el Terminal Web. %{helpStart}Aprende más.%{helpEnd}" -"Depends on <strong>%d closed</strong> merge request.": - plural_id: "Depends on <strong>%d closed</strong> merge requests." - translations: - - "В зависимости от <strong>%d закрытого</strong> запроса на слияние." - - "В зависимости от <strong>%d закрытых</strong> запросов на слияние." - - "В зависимости от <strong>%d закрытых</strong> запросов на слияние." - - "В зависимости от <strong>%d закрытых</strong> запросов на слияние." - - "依赖于<strong>%d个已关闭的</strong>合并请求" - - "Залежить від %d <strong>закритого</strong> запиту на злиття." - - "Залежить від %d <strong>закритих</strong> запитів на злиття." - - "Залежить від %d <strong>закритих</strong> запитів на злиття." - - "Залежить від %d <strong>закритих</strong> запитів на злиття." - - "<strong>%d kapanan</strong> birleştirme isteğine bağlıdır." - - "<strong>%d kapanan</strong> birleştirme isteğine bağlıdır." -"Go to <strong>Issues</strong> > <strong>Boards</strong> to access your personalized learning issue board.": - plural_id: - translations: - - "转至<strong>议题</strong> > <strong>看板</strong>访问您的个性化学习议题看板。" -"Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>": - plural_id: - translations: - - "<span>要讓標籤</span> %{labelTitle} <span>提升到群組標籤嗎?</span>" - - "<span>Promover a etiqueta</span> %{labelTitle} <span>para etiqueta do Grupo?</span>" - - "%{labelTitle} <span>ラベルをグループラベルに昇格しますか?</span>" - - "<span>Повысить метку</span> %{labelTitle} <span>до групповой метки?</span>" - - "<span>将标记</span> %{labelTitle} <span>升级为群组标记?</span>" - - "<span>Перенести мітку</span> %{labelTitle} <span>на рівень групи?</span>" - - "<span>Label</span> %{labelTitle} <span>zu Gruppenlabel hochstufen?</span>" - - "<span>라벨</span> %{labelTitle} <span>(을)를 그룹 라벨로 승격하시겠습니까?</span>" - - "<span>Promouvoir l’étiquette</span> %{labelTitle} <span>en étiquette de groupe ?</span>" - - "<span>¿Promocionar la etiqueta</span> %{labelTitle} <span>a etiqueta de grupo?</span>" -"Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.": - plural_id: - translations: - - "Travar este %{issuableDisplayName}? Apenas <strong>membros do projeto</strong> poderão comentar." - - "%{issuableDisplayName} をロックしますか?<strong>プロジェクトメンバー</strong> のみコメントできます。" - - "锁定此%{issuableDisplayName}吗?锁定后将只有<strong>项目成员</strong>可以发表评论。" - - "Заблокувати цю %{issuableDisplayName}? Лише <strong>учасники проекту</strong> зможуть коментувати." - - "%{issuableDisplayName} sperren? Es werden nur noch <strong>Projektmitglieder</strong> kommentieren können." - - "Verrouiller ce·t·te %{issuableDisplayName} ? Seuls les <strong>membres du projet</strong> seront en mesure de commenter." - - "¿Bloquear este %{issuableDisplayName}? Sólo los <strong>miembros del proyecto</strong> podrán comentar." -"PrometheusService|<p class=\\\"text-tertiary\\\">No <a href=\\\"%{docsUrl}\\\">common metrics</a> were found</p>": - plural_id: - translations: - - "<p class=\\\"text-tertiary\\\">Nenhuma <a href=\\\"%{docsUrl}\\\">métrica comum</a> foi encontrada</p>" - - "<p class=\\\"text-tertiary\\\"><a href=\\\"%{docsUrl}\\\">共通メトリクス</a>は見つかりませんでした</p>" - - "<p class=\\\"text-tertiary\\\">Ни одной <a href=\\\"%{docsUrl}\\\">общей метрики</a> не найдено</p>" - - "<p class=\\\"text-tertiary\\\">无<a href=\\\"%{docsUrl}\\\">常用指标</a> </p>" - - "<p class=\\\"text-tertiary\\\">Ніяких <a href=\\\"%{docsUrl}\\\">загальних метрик</a> не знайдено</p>" - - "<p class=\\\"text-tertiary\\\">Es wurden keine <a href=\\\"%{docsUrl}\\\">allgemeinen Metriken</a> gefunden</p>" - - "<p class=\\\"text-tertiary\\\"><a href=\\\"%{docsUrl}\\\">공통 메트릭스</a>가 발견되지 않았습니다.</p>" - - "<p class=\\\"text-tertiary\\\">Aucune <a href=\\\"%{docsUrl}\\\">métrique commune</a> trouvée</p>" - - "<p class=\\\"text-tertiary\\\">No se han encontrado<a href=\\\"%{docsUrl}\\\">métricas comunes</a> </p>" -"This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">enable billing <i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> and try again.": - plural_id: - translations: - - "Este projeto não possui faturamento ativado. Para criar um cluster, <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">ative o faturamento <i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> e tente novamente." - - "このプロジェクトでは課金が有効になっていません。クラスターを作成するには、<a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\"> 課金を有効<i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> にして再度お試しください。" - - "此项目未启用账单。要创建群集,请 <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">启用账单 <i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> 并重试。" - - "Для цього проекту вимкнено білінг. Щоб створити кластер, <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">увімкніть білінг <i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> і спробуйте знову." - - "Für dieses Projekt ist keine Abrechnung aktiviert. Um ein Cluster zu erstellen, <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">aktiviere die Abrechnung<i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> und versuche es erneut." - - "Ce projet n’a pas de facturation activée. Afin de créer une grappe de serveurs, veuillez <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">activer la facturation<i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> et réessayer." - - "Este proyecto no tiene la facturación habilitada. Para crear un clúster, <a href=%{linkToBilling} target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">habilite la facturación <i class=\\\"fa fa-external-link\\\" aria-hidden=\\\"true\\\"></i></a> e inténtelo de nuevo." -"Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.": - plural_id: - translations: - - "Desbloquear este %{issuableDisplayName}? <strong>Todos</strong> poderão comentar." - - "%{issuableDisplayName} のロックを解除しますか? <strong>全員</strong>がコメントできるようになります。" - - "解锁此%{issuableDisplayName}吗?解锁后<strong>所有人</strong>都将可以发表评论。" - - "Розблокувати %{issuableDisplayName}? <strong>Будь-хто</strong> зможе залишати коментарі." - - "Dieses %{issuableDisplayName} entsperren? <strong>Jeder</strong> wird in der Lage sein zu kommentieren." - - "%{issuableDisplayName}(을)를 잠금해제 하시겠습니까? <strong>모두가</strong> 코멘트 할 수 있게 됩니다." - - "Déverrouiller %{issuableDisplayName} ? <strong>Tout le monde</strong> sera en mesure de commenter." - - "Desbloquear este %{issuableDisplayName}? <strong>Todos</strong> podrán comentar." -"confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.": - plural_id: - translations: - - "Você está prestes a desligar a confidencialidade. Isso significa que <strong>todos</strong> serão capazes de ver e deixar comentários nesse issue." - - "あなたは公開設定に変更しようとしています。これは<strong>すべての人</strong> が閲覧可能になり、課題に対してコメントを残すことができるようになることを意味します。" - - "即将关闭私密性。这将使得 <strong>所有用户</strong>都可以查看并且评论当前议题。" - - "Ви вимикаєте конфіденційність. Це означає, що <strong>будь-хто</strong> зможе бачити і залишати коментарі для цієї задачі." - - "Du willst die Vertraulichkeit deaktivieren. Das bedeutet, dass <strong>alle</strong> das Ticket betrachten und kommentieren können." - - "Vous êtes sur le point de désactiver la confidentialité. Cela signifie que <strong>tout le monde</strong> sera en mesure de voir et de laisser un commentaire sur ce ticket." - - "Va a desactivar la confidencialidad. Esto significa que <strong>todos</strong> podrán ver y dejar un comentario sobre este tema." -"confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.": - plural_id: - translations: - - "Você está prestes a ligar a confidencialidade. Isso significa que apenas membros da equipe com <strong>ao menos acesso de Relator</strong> serão capazes de ver e deixar comentários nesse issue." - - "あなたは公開設定に変更しようとしています。これはチームに限定していた<strong>最小限の報告権限</strong>をなくし、課題に対してコメントを残すことができるようになることを意味します。" - - "即将设置私密性。这将使得 <strong>至少有Reporter以上权限</strong>的团队成员才能查看并且评论当前议题。" - - "Ви вмикаєте конфіденційність. Це означає що лише учасники команди <strong>рівня репортер або вище</strong> матимуть змогу бачити та залишати коментарі для цієї задачі." - - "Du willst die Vertraulichkeit aktivieren. Das bedeutet, dass nur Teammitglieder mit <strong>mindestens Reporter-Zugriff</strong> das Ticket betrachten und kommentieren können." - - "Vous êtes sur le point de d’activer la confidentialité. Cela signifie que seuls les membres de l’équipe avec <strong>au moins un accès en tant que rapporteur</strong> seront en mesure de voir et de laisser des commentaires sur le ticket." - - "Va a activar la confidencialidad. Esto significa que solo los miembros del equipo con como mínimo,<strong>acceso como Reporter</strong> podrán ver y dejar comentarios sobre la incidencia." - - "あなたは非公開設定をオンにしようとしています。これは、最低でも<strong>報告権限</strong>を持ったチームメンバーのみが課題を表示したりコメントを残したりすることができるようになるということです。" -" or <!merge request id>": - translations: - - " ወይም <!merge request id>" - - " ou <!merge request id>" - - " または <!merge request id>" - - "或 <!合併請求 id>" - - " или <!merge request id>" - - "或<!merge request id>" - - " або <!merge request id>" - - " oder <!merge request id>" - - " o <!merge request id>" - - " 또는 <!merge request id>" - - " o <!merge request id>" - - " veya <!merge request id>" - - " neu <!merge request id>" - - " neu <#issue id>" -" or <#issue id>": - translations: - - "或 <#issue id>" - - " ወይም ‹#issue id›" - - " ou <identificación #issue>" - - " ou <#issue id>" - - " または <#課題 ID>" - - " o <#issue id>" - - "或 <#議題 id>" - - " ou <#issue id>" - - " или <#issue id>" - - "或 <#issue id>" - - " або <#issue id>" - - " oder <#issue id>" - - " o <#issue id>" - - " 또는 <#issue id>" - - " ou <#issue id>" - - " o <#issue id>" - - " veya <#issue id>" - - " neu <#issue id>" -" or <&epic id>": - translations: - - " ወይም <&epic id>" - - " または <&エピックID>" - - " 或 <#史詩 id>" - - " или <&epic id>" - - " 或<#epic id>" - - " або <&epic id>" - - " oder <&epic id>" - - " o <&epic id>" - - " veya <&epic id>" - - " neu <#epic id>" - - " 또는 <&epic id>" -"< 1 hour": - translations: - - "1 時間未満" - - "< 1 小時" - - "< 1 часа" - - "< 1小时" - - "< 1 години" - - "< 1 hora" - - "< 1 saat" - - "< 1 Stunde" - - "< 1시간" diff --git a/lib/gitlab/i18n/po_linter.rb b/lib/gitlab/i18n/po_linter.rb index 33054a5b9bf..3bb34ab2811 100644 --- a/lib/gitlab/i18n/po_linter.rb +++ b/lib/gitlab/i18n/po_linter.rb @@ -5,14 +5,13 @@ module Gitlab class PoLinter include Gitlab::Utils::StrongMemoize - attr_reader :po_path, :translation_entries, :metadata_entry, :locale, :html_todolist + attr_reader :po_path, :translation_entries, :metadata_entry, :locale VARIABLE_REGEX = /%{\w*}|%[a-z]/.freeze - def initialize(po_path:, html_todolist:, locale: I18n.locale.to_s) + def initialize(po_path:, locale: I18n.locale.to_s) @po_path = po_path @locale = locale - @html_todolist = html_todolist end def errors @@ -43,8 +42,7 @@ module Gitlab @translation_entries = entries.map do |entry_data| Gitlab::I18n::TranslationEntry.new( entry_data: entry_data, - nplurals: metadata_entry.expected_forms, - html_allowed: html_todolist.fetch(entry_data[:msgid], false) + nplurals: metadata_entry.expected_forms ) end @@ -97,15 +95,15 @@ module Gitlab common_message = 'contains < or >. Use variables to include HTML in the string, or the < and > codes ' \ 'for the symbols. For more info see: https://docs.gitlab.com/ee/development/i18n/externalization.html#html' - if entry.msgid_contains_potential_html? && !entry.msgid_html_allowed? + if entry.msgid_contains_potential_html? errors << common_message end - if entry.plural_id_contains_potential_html? && !entry.plural_id_html_allowed? + if entry.plural_id_contains_potential_html? errors << 'plural id ' + common_message end - if entry.translations_contain_potential_html? && !entry.translations_html_allowed? + if entry.translations_contain_potential_html? errors << 'translation ' + common_message end end diff --git a/lib/gitlab/i18n/translation_entry.rb b/lib/gitlab/i18n/translation_entry.rb index 25a45332d27..f3cca97950d 100644 --- a/lib/gitlab/i18n/translation_entry.rb +++ b/lib/gitlab/i18n/translation_entry.rb @@ -6,12 +6,11 @@ module Gitlab PERCENT_REGEX = /(?:^|[^%])%(?!{\w*}|[a-z%])/.freeze ANGLE_BRACKET_REGEX = /[<>]/.freeze - attr_reader :nplurals, :entry_data, :html_allowed + attr_reader :nplurals, :entry_data - def initialize(entry_data:, nplurals:, html_allowed:) + def initialize(entry_data:, nplurals:) @entry_data = entry_data @nplurals = nplurals - @html_allowed = html_allowed end def msgid @@ -97,20 +96,6 @@ module Gitlab all_translations.any? { |translation| contains_angle_brackets?(translation) } end - def msgid_html_allowed? - html_allowed.present? - end - - def plural_id_html_allowed? - html_allowed.present? && html_allowed['plural_id'] == plural_id - end - - def translations_html_allowed? - msgid_html_allowed? && html_allowed['translations'].present? && all_translations.all? do |translation| - html_allowed['translations'].include?(translation) - end - end - private def contains_angle_brackets?(string) diff --git a/lib/gitlab/import_export/group/tree_restorer.rb b/lib/gitlab/import_export/group/tree_restorer.rb index d0c0999f291..dfe27118d66 100644 --- a/lib/gitlab/import_export/group/tree_restorer.rb +++ b/lib/gitlab/import_export/group/tree_restorer.rb @@ -74,6 +74,12 @@ module Gitlab group = create_group(group_attributes) restore_group(group, group_attributes) + rescue => e + import_failure_service.log_import_failure( + source: 'process_child', + relation_key: 'group', + exception: e + ) end def create_group(group_attributes) @@ -83,13 +89,17 @@ module Gitlab parent_group = @groups_mapping.fetch(parent_id) { raise(ArgumentError, 'Parent group not found') } - ::Groups::CreateService.new( + group = ::Groups::CreateService.new( user, name: name, path: path, parent_id: parent_group.id, visibility_level: sub_group_visibility_level(group_attributes.attributes, parent_group) ).execute + + group.validate! + + group end def restore_group(group, group_attributes) @@ -134,6 +144,10 @@ module Gitlab ) end end + + def import_failure_service + Gitlab::ImportExport::ImportFailureService.new(@top_level_group) + end end end end diff --git a/lib/gitlab/import_export/import_failure_service.rb b/lib/gitlab/import_export/import_failure_service.rb index d4eca551b49..bf7200726a1 100644 --- a/lib/gitlab/import_export/import_failure_service.rb +++ b/lib/gitlab/import_export/import_failure_service.rb @@ -28,23 +28,26 @@ module Gitlab end def log_import_failure(source:, relation_key: nil, relation_index: nil, exception:, retry_count: 0) - extra = { - source: source, - relation_key: relation_key, + attributes = { relation_index: relation_index, - retry_count: retry_count + source: source, + retry_count: retry_count, + importable_column_name => importable.id } - extra[importable_column_name] = importable.id - - Gitlab::ErrorTracking.track_exception(exception, extra) - - attributes = { - exception_class: exception.class.to_s, - exception_message: exception.message.truncate(255), - correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id - }.merge(extra) - ImportFailure.create(attributes) + Gitlab::ErrorTracking.track_exception( + exception, + attributes.merge(relation_name: relation_key) + ) + + ImportFailure.create( + attributes.merge( + exception_class: exception.class.to_s, + exception_message: exception.message.truncate(255), + correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id, + relation_key: relation_key + ) + ) end private diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb index 8e78f6e274a..789249c7d91 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -79,10 +79,9 @@ module Gitlab end def wiki_restorer - Gitlab::ImportExport::WikiRestorer.new(path_to_bundle: wiki_repo_path, + Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path, shared: shared, - project: ProjectWiki.new(project), - wiki_enabled: project.wiki_enabled?) + project: ProjectWiki.new(project)) end def design_repo_restorer diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index ae7ddbc5eba..778b42f4358 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -169,6 +169,7 @@ excluded_attributes: - :compliance_framework_setting - :show_default_award_emojis - :services + - :exported_protected_branches namespaces: - :runners_token - :runners_token_encrypted @@ -219,6 +220,7 @@ excluded_attributes: - :duplicated_to_id - :promoted_to_epic_id - :blocking_issues_count + - :service_desk_reply_to merge_request: - :milestone_id - :sprint_id @@ -340,10 +342,12 @@ excluded_attributes: - :protected_environment_id boards: - :milestone_id + - :iteration_id lists: - :board_id - :label_id - :milestone_id + - :iteration_id epic: - :start_date_sourcing_milestone_id - :due_date_sourcing_milestone_id diff --git a/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb b/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb index 44ccb67a531..4db92b12968 100644 --- a/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb +++ b/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb @@ -5,8 +5,8 @@ module Gitlab module Project module Sample class RelationTreeRestorer < ImportExport::RelationTreeRestorer - def initialize(*args) - super + def initialize(...) + super(...) @date_calculator = Gitlab::ImportExport::Project::Sample::DateCalculator.new(dates) end diff --git a/lib/gitlab/import_export/relation_tree_restorer.rb b/lib/gitlab/import_export/relation_tree_restorer.rb index ea16d978127..8bc87ecb071 100644 --- a/lib/gitlab/import_export/relation_tree_restorer.rb +++ b/lib/gitlab/import_export/relation_tree_restorer.rb @@ -71,7 +71,7 @@ module Gitlab end def process_relation_item!(relation_key, relation_definition, relation_index, data_hash) - relation_object = build_relation(relation_key, relation_definition, data_hash) + relation_object = build_relation(relation_key, relation_definition, relation_index, data_hash) return unless relation_object return if importable_class == ::Project && group_model?(relation_object) @@ -139,23 +139,35 @@ module Gitlab end end - def build_relations(relation_key, relation_definition, data_hashes) + def build_relations(relation_key, relation_definition, relation_index, data_hashes) data_hashes - .map { |data_hash| build_relation(relation_key, relation_definition, data_hash) } + .map { |data_hash| build_relation(relation_key, relation_definition, relation_index, data_hash) } .tap { |entries| entries.compact! } end - def build_relation(relation_key, relation_definition, data_hash) + def build_relation(relation_key, relation_definition, relation_index, data_hash) # TODO: This is hack to not create relation for the author # Rather make `RelationFactory#set_note_author` to take care of that return data_hash if relation_key == 'author' || already_restored?(data_hash) # create relation objects recursively for all sub-objects relation_definition.each do |sub_relation_key, sub_relation_definition| - transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition) + transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index) end - @relation_factory.create(relation_factory_params(relation_key, data_hash)) + relation = @relation_factory.create(**relation_factory_params(relation_key, data_hash)) + + if relation && !relation.valid? + @shared.logger.warn( + message: "[Project/Group Import] Invalid object relation built", + relation_key: relation_key, + relation_index: relation_index, + relation_class: relation.class.name, + error_messages: relation.errors.full_messages.join(". ") + ) + end + + relation end # Since we update the data hash in place as we restore relation items, @@ -165,7 +177,7 @@ module Gitlab !relation_item.is_a?(Hash) end - def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition) + def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index) sub_data_hash = data_hash[sub_relation_key] return unless sub_data_hash @@ -176,11 +188,13 @@ module Gitlab build_relations( sub_relation_key, sub_relation_definition, + relation_index, sub_data_hash).presence else build_relation( sub_relation_key, sub_relation_definition, + relation_index, sub_data_hash) end diff --git a/lib/gitlab/import_export/wiki_restorer.rb b/lib/gitlab/import_export/wiki_restorer.rb deleted file mode 100644 index 359ba8ba769..00000000000 --- a/lib/gitlab/import_export/wiki_restorer.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module ImportExport - class WikiRestorer < RepoRestorer - def initialize(project:, shared:, path_to_bundle:, wiki_enabled:) - super(project: project, shared: shared, path_to_bundle: path_to_bundle) - - @project = project - @wiki_enabled = wiki_enabled - end - - def restore - project.wiki if create_empty_wiki? - - super - end - - private - - attr_accessor :project, :wiki_enabled - - def create_empty_wiki? - !File.exist?(path_to_bundle) && wiki_enabled - end - end - end -end diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb index 58c7744fae0..88753e80391 100644 --- a/lib/gitlab/import_sources.rb +++ b/lib/gitlab/import_sources.rb @@ -15,7 +15,7 @@ module Gitlab ImportSource.new('bitbucket', 'Bitbucket Cloud', Gitlab::BitbucketImport::Importer), ImportSource.new('bitbucket_server', 'Bitbucket Server', Gitlab::BitbucketServerImport::Importer), ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer), - ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer), + ImportSource.new('google_code', 'Google Code', nil), ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer), ImportSource.new('git', 'Repo by URL', nil), ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer), diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb index d7228099eaf..6b0f01757b7 100644 --- a/lib/gitlab/instrumentation_helper.rb +++ b/lib/gitlab/instrumentation_helper.rb @@ -13,7 +13,8 @@ module Gitlab :rugged_duration_s, :elasticsearch_calls, :elasticsearch_duration_s, - *::Gitlab::Instrumentation::Redis.known_payload_keys] + *::Gitlab::Instrumentation::Redis.known_payload_keys, + *::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS] end def add_instrumentation_data(payload) @@ -22,6 +23,7 @@ module Gitlab instrument_redis(payload) instrument_elasticsearch(payload) instrument_throttle(payload) + instrument_active_record(payload) end def instrument_gitaly(payload) @@ -62,6 +64,12 @@ module Gitlab payload[:throttle_safelist] = safelist if safelist.present? end + def instrument_active_record(payload) + db_counters = ::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_payload + + payload.merge!(db_counters) + end + # Returns the queuing duration for a Sidekiq job in seconds, as a float, if the # `enqueued_at` field or `created_at` field is available. # diff --git a/lib/gitlab/kroki.rb b/lib/gitlab/kroki.rb new file mode 100644 index 00000000000..8c5652fb766 --- /dev/null +++ b/lib/gitlab/kroki.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'asciidoctor/extensions/asciidoctor_kroki/extension' + +module Gitlab + # Helper methods for Kroki + module Kroki + BLOCKDIAG_FORMATS = %w[ + blockdiag + seqdiag + actdiag + nwdiag + packetdiag + rackdiag + ].freeze + # Diagrams that require a companion container are disabled for now + DIAGRAMS_FORMATS = ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES + .reject { |diagram_type| diagram_type == 'mermaid' || diagram_type == 'bpmn' || BLOCKDIAG_FORMATS.include?(diagram_type) } + DIAGRAMS_FORMATS_WO_PLANTUML = DIAGRAMS_FORMATS + .reject { |diagram_type| diagram_type == 'plantuml' } + + # Get the list of diagram formats that are currently enabled + # + # Returns an Array of diagram formats. + # If Kroki is not enabled, returns an empty Array. + def self.formats(current_settings) + return [] unless current_settings.kroki_enabled + + # If PlantUML is enabled, PlantUML diagrams will be processed by the PlantUML server. + # In other words, the PlantUML server has precedence over Kroki since both can process PlantUML diagrams. + if current_settings.plantuml_enabled + DIAGRAMS_FORMATS_WO_PLANTUML + else + DIAGRAMS_FORMATS + end + end + end +end diff --git a/lib/gitlab/kubernetes/deployment.rb b/lib/gitlab/kubernetes/deployment.rb new file mode 100644 index 00000000000..55ed9a7517e --- /dev/null +++ b/lib/gitlab/kubernetes/deployment.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + class Deployment + include Gitlab::Utils::StrongMemoize + + STABLE_TRACK_VALUE = 'stable'.freeze + + def initialize(attributes = {}, pods: []) + @attributes = attributes + @pods = pods + end + + def name + metadata['name'] || 'unknown' + end + + def labels + metadata.fetch('labels', {}) + end + + def annotations + metadata.fetch('annotations', {}) + end + + def track + labels.fetch('track', STABLE_TRACK_VALUE) + end + + def stable? + track == 'stable' + end + + def order + stable? ? 1 : 0 + end + + def outdated? + observed_generation < generation + end + + def wanted_instances + spec.fetch('replicas', 0) + end + + def created_instances + filtered_pods_by_track.map do |pod| + pod_metadata = pod.fetch('metadata', {}) + pod_name = pod_metadata['name'] || pod_metadata['generateName'] + pod_status = pod.dig('status', 'phase') + + deployment_instance(pod_name: pod_name, pod_status: pod_status) + end + end + + # These are replicas that did not get created yet, + # So they still do not have any associated pod, + # these are marked as pending instances. + def not_created_instances + pending_instances_count = wanted_instances - filtered_pods_by_track.count + + return [] if pending_instances_count <= 0 + + Array.new(pending_instances_count, deployment_instance(pod_name: 'Not provided', pod_status: 'Pending')) + end + + def filtered_pods_by_track + strong_memoize(:filtered_pods_by_track) do + @pods.select { |pod| has_same_track?(pod) } + end + end + + def instances + created_instances + not_created_instances + end + + private + + def deployment_instance(pod_name:, pod_status:) + { + status: pod_status&.downcase, + pod_name: pod_name, + tooltip: "#{pod_name} (#{pod_status})", + track: track, + stable: stable? + } + end + + def has_same_track?(pod) + pod_track = pod.dig('metadata', 'labels', 'track') || STABLE_TRACK_VALUE + + pod_track == track + end + + def metadata + @attributes.fetch('metadata', {}) + end + + def spec + @attributes.fetch('spec', {}) + end + + def status + @attributes.fetch('status', {}) + end + + def generation + metadata.fetch('generation', 0) + end + + def observed_generation + status.fetch('observedGeneration', 0) + end + end + end +end diff --git a/lib/gitlab/kubernetes/helm/v2/client_command.rb b/lib/gitlab/kubernetes/helm/v2/client_command.rb index 88693a28d6c..8b15af9aeea 100644 --- a/lib/gitlab/kubernetes/helm/v2/client_command.rb +++ b/lib/gitlab/kubernetes/helm/v2/client_command.rb @@ -22,17 +22,6 @@ module Gitlab def repository_update_command 'helm repo update' end - - def optional_tls_flags - return [] unless files.key?(:'ca.pem') - - [ - '--tls', - '--tls-ca-cert', "#{files_dir}/ca.pem", - '--tls-cert', "#{files_dir}/cert.pem", - '--tls-key', "#{files_dir}/key.pem" - ] - end end end end diff --git a/lib/gitlab/kubernetes/helm/v2/reset_command.rb b/lib/gitlab/kubernetes/helm/v2/reset_command.rb index 172a0884c49..00626501a9a 100644 --- a/lib/gitlab/kubernetes/helm/v2/reset_command.rb +++ b/lib/gitlab/kubernetes/helm/v2/reset_command.rb @@ -9,9 +9,8 @@ module Gitlab def generate_script super + [ - reset_helm_command, - delete_tiller_replicaset, - delete_tiller_clusterrolebinding + init_command, + reset_helm_command ].join("\n") end @@ -21,27 +20,8 @@ module Gitlab private - # This method can be delete once we upgrade Helm to > 12.13.0 - # https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/27096#note_159695900 - # - # Tracking this method to be removed here: - # https://gitlab.com/gitlab-org/gitlab-foss/issues/52791#note_199374155 - def delete_tiller_replicaset - delete_args = %w[replicaset -n gitlab-managed-apps -l name=tiller] - - Gitlab::Kubernetes::KubectlCmd.delete(*delete_args) - end - - def delete_tiller_clusterrolebinding - delete_args = %w[clusterrolebinding tiller-admin] - - Gitlab::Kubernetes::KubectlCmd.delete(*delete_args) - end - def reset_helm_command - command = %w[helm reset] + optional_tls_flags - - command.shelljoin + 'helm reset --force' end end end diff --git a/lib/gitlab/kubernetes/ingress.rb b/lib/gitlab/kubernetes/ingress.rb new file mode 100644 index 00000000000..c5643dd670a --- /dev/null +++ b/lib/gitlab/kubernetes/ingress.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + class Ingress + include Gitlab::Utils::StrongMemoize + + # Canary Ingress Annotations https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#canary + ANNOTATION_KEY_CANARY = 'nginx.ingress.kubernetes.io/canary' + ANNOTATION_KEY_CANARY_WEIGHT = 'nginx.ingress.kubernetes.io/canary-weight' + + def initialize(attributes = {}) + @attributes = attributes + end + + def canary? + strong_memoize(:is_canary) do + annotations.any? do |key, value| + key == ANNOTATION_KEY_CANARY && value == 'true' + end + end + end + + def canary_weight + return unless canary? + return unless annotations.key?(ANNOTATION_KEY_CANARY_WEIGHT) + + annotations[ANNOTATION_KEY_CANARY_WEIGHT].to_i + end + + def name + metadata['name'] + end + + private + + def metadata + @attributes.fetch('metadata', {}) + end + + def annotations + metadata.fetch('annotations', {}) + end + end + end +end diff --git a/lib/gitlab/kubernetes/rollout_instances.rb b/lib/gitlab/kubernetes/rollout_instances.rb new file mode 100644 index 00000000000..c5dba71f505 --- /dev/null +++ b/lib/gitlab/kubernetes/rollout_instances.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + class RolloutInstances + include ::Gitlab::Utils::StrongMemoize + + def initialize(deployments, pods) + @deployments = deployments + @pods = pods + end + + def pod_instances + pods = matching_pods + extra_pending_pods + + pods.sort_by(&:order).map do |pod| + to_hash(pod) + end + end + + private + + attr_reader :deployments, :pods + + def matching_pods + strong_memoize(:matching_pods) do + deployment_tracks = deployments.map(&:track) + pods.select { |p| deployment_tracks.include?(p.track) } + end + end + + def extra_pending_pods + wanted_instances = sum_hashes(deployments.map { |d| { d.track => d.wanted_instances } }) + present_instances = sum_hashes(matching_pods.map { |p| { p.track => 1 } }) + pending_instances = subtract_hashes(wanted_instances, present_instances) + + pending_instances.flat_map do |track, num| + Array.new(num, pending_pod_for(track)) + end + end + + def sum_hashes(hashes) + hashes.reduce({}) do |memo, hash| + memo.merge(hash) { |_key, memo_val, hash_val| memo_val + hash_val } + end + end + + def subtract_hashes(hash_a, hash_b) + hash_a.merge(hash_b) { |_key, val_a, val_b| [0, val_a - val_b].max } + end + + def pending_pod_for(track) + ::Gitlab::Kubernetes::Pod.new({ + 'status' => { 'phase' => 'Pending' }, + 'metadata' => { + 'name' => 'Not provided', + 'labels' => { + 'track' => track + } + } + }) + end + + def to_hash(pod) + { + status: pod.status&.downcase, + pod_name: pod.name, + tooltip: "#{pod.name} (#{pod.status})", + track: pod.track, + stable: pod.stable? + } + end + end + end +end diff --git a/lib/gitlab/kubernetes/rollout_status.rb b/lib/gitlab/kubernetes/rollout_status.rb new file mode 100644 index 00000000000..e275303e650 --- /dev/null +++ b/lib/gitlab/kubernetes/rollout_status.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + # Calculates the rollout status for a set of kubernetes deployments. + # + # A GitLab environment may be composed of several Kubernetes deployments and + # other resources. The rollout status sums the Kubernetes deployments + # together. + class RolloutStatus + attr_reader :deployments, :instances, :completion, :status, :canary_ingress + + def complete? + completion == 100 + end + + def loading? + @status == :loading + end + + def not_found? + @status == :not_found + end + + def found? + @status == :found + end + + def canary_ingress_exists? + canary_ingress.present? + end + + def self.from_deployments(*deployments_attrs, pods_attrs: [], ingresses: []) + return new([], status: :not_found) if deployments_attrs.empty? + + deployments = deployments_attrs.map do |attrs| + ::Gitlab::Kubernetes::Deployment.new(attrs, pods: pods_attrs) + end + deployments.sort_by!(&:order) + + pods = pods_attrs.map do |attrs| + ::Gitlab::Kubernetes::Pod.new(attrs) + end + + ingresses = ingresses.map { |ingress| ::Gitlab::Kubernetes::Ingress.new(ingress) } + + new(deployments, pods: pods, ingresses: ingresses) + end + + def self.loading + new([], status: :loading) + end + + def initialize(deployments, pods: [], ingresses: [], status: :found) + @status = status + @deployments = deployments + @instances = RolloutInstances.new(deployments, pods).pod_instances + @canary_ingress = ingresses.find(&:canary?) + + @completion = + if @instances.empty? + 100 + else + # We downcase the pod status in Gitlab::Kubernetes::Deployment#deployment_instance + finished = @instances.count { |instance| instance[:status] == ::Gitlab::Kubernetes::Pod::RUNNING.downcase } + + (finished / @instances.count.to_f * 100).to_i + end + end + end + end +end diff --git a/lib/gitlab/legacy_github_import/project_creator.rb b/lib/gitlab/legacy_github_import/project_creator.rb index b484b69c932..c54325bcdf5 100644 --- a/lib/gitlab/legacy_github_import/project_creator.rb +++ b/lib/gitlab/legacy_github_import/project_creator.rb @@ -5,7 +5,7 @@ module Gitlab class ProjectCreator attr_reader :repo, :name, :namespace, :current_user, :session_data, :type - def initialize(repo, name, namespace, current_user, session_data, type: 'github') + def initialize(repo, name, namespace, current_user, type: 'github', **session_data) @repo = repo @name = name @namespace = namespace diff --git a/lib/gitlab/markdown_cache/active_record/extension.rb b/lib/gitlab/markdown_cache/active_record/extension.rb index 233d3bf1ac7..1de890c84f9 100644 --- a/lib/gitlab/markdown_cache/active_record/extension.rb +++ b/lib/gitlab/markdown_cache/active_record/extension.rb @@ -10,6 +10,7 @@ module Gitlab # Using before_update here conflicts with elasticsearch-model somehow before_create :refresh_markdown_cache, if: :invalidated_markdown_cache? before_update :refresh_markdown_cache, if: :invalidated_markdown_cache? + after_save :store_mentions!, if: :mentionable_attributes_changed? end # Always exclude _html fields from attributes (including serialization). diff --git a/lib/gitlab/metrics/background_transaction.rb b/lib/gitlab/metrics/background_transaction.rb deleted file mode 100644 index 7b05ae29b02..00000000000 --- a/lib/gitlab/metrics/background_transaction.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Metrics - class BackgroundTransaction < Transaction - def initialize(worker_class) - super() - @worker_class = worker_class - end - - def labels - { controller: @worker_class.name, action: 'perform', feature_category: @worker_class.try(:get_feature_category).to_s } - end - end - end -end diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb deleted file mode 100644 index 8c4e5a8d70c..00000000000 --- a/lib/gitlab/metrics/sidekiq_middleware.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Metrics - # Sidekiq middleware for tracking jobs. - # - # This middleware is intended to be used as a server-side middleware. - class SidekiqMiddleware - def call(worker, payload, queue) - trans = BackgroundTransaction.new(worker.class) - - begin - # Old gitlad-shell messages don't provide enqueued_at/created_at attributes - enqueued_at = payload['enqueued_at'] || payload['created_at'] || 0 - trans.set(:gitlab_transaction_sidekiq_queue_duration_total, Time.current.to_f - enqueued_at) do - multiprocess_mode :livesum - end - trans.run { yield } - rescue Exception => error # rubocop: disable Lint/RescueException - trans.add_event(:sidekiq_exception) - - raise error - ensure - add_info_to_payload(payload, trans) - end - end - - private - - def add_info_to_payload(payload, trans) - payload.merge!(::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_payload) - end - end - end -end diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index f9ba0a69b0e..d725d8d7b29 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -16,16 +16,14 @@ module Gitlab # using a connection. Thread.current[:uses_db_connection] = true - return unless current_transaction - payload = event.payload return if payload[:name] == 'SCHEMA' || IGNORABLE_SQL.include?(payload[:sql]) - current_transaction.observe(:gitlab_sql_duration_seconds, event.duration / 1000.0) do + increment_db_counters(payload) + + current_transaction&.observe(:gitlab_sql_duration_seconds, event.duration / 1000.0) do buckets [0.05, 0.1, 0.25] end - - increment_db_counters(payload) end def self.db_counter_payload @@ -53,7 +51,7 @@ module Gitlab end def increment(counter) - current_transaction.increment("gitlab_transaction_#{counter}_total".to_sym, 1) + current_transaction&.increment("gitlab_transaction_#{counter}_total".to_sym, 1) if Gitlab::SafeRequestStore.active? Gitlab::SafeRequestStore[counter] = Gitlab::SafeRequestStore[counter].to_i + 1 diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index 95bc90f9dad..3ebafb5c5e4 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -48,23 +48,15 @@ module Gitlab @finished_at ? (@finished_at - @started_at) : 0.0 end - def thread_cpu_duration - System.thread_cpu_duration(@thread_cputime_start) - end - def run Thread.current[THREAD_KEY] = self @started_at = System.monotonic_time - @thread_cputime_start = System.thread_cpu_time yield ensure @finished_at = System.monotonic_time - observe(:gitlab_transaction_cputime_seconds, thread_cpu_duration) do - buckets SMALL_BUCKETS - end observe(:gitlab_transaction_duration_seconds, duration) do buckets SMALL_BUCKETS end diff --git a/lib/gitlab/metrics/web_transaction.rb b/lib/gitlab/metrics/web_transaction.rb index 2064f9290d3..1811389a744 100644 --- a/lib/gitlab/metrics/web_transaction.rb +++ b/lib/gitlab/metrics/web_transaction.rb @@ -66,9 +66,10 @@ module Gitlab if route path = endpoint_paths_cache[route.request_method][route.path] - # Feature categories will be added for grape endpoints in - # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/462 - { controller: 'Grape', action: "#{route.request_method} #{path}", feature_category: '' } + grape_class = endpoint.options[:for] + feature_category = grape_class.try(:feature_category_for_app, endpoint).to_s + + { controller: 'Grape', action: "#{route.request_method} #{path}", feature_category: feature_category } end end diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb index 101172cdfcc..b11ee0afc10 100644 --- a/lib/gitlab/middleware/read_only/controller.rb +++ b/lib/gitlab/middleware/read_only/controller.rb @@ -9,7 +9,7 @@ module Gitlab APPLICATION_JSON_TYPES = %W{#{APPLICATION_JSON} application/vnd.git-lfs+json}.freeze ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance' - ALLOWLISTED_GIT_ROUTES = { + ALLOWLISTED_GIT_READ_ONLY_ROUTES = { 'repositories/git_http' => %w{git_upload_pack} }.freeze @@ -34,7 +34,7 @@ module Gitlab end def call - if disallowed_request? && Gitlab::Database.read_only? + if disallowed_request? && read_only? Gitlab::AppLogger.debug('GitLab ReadOnly: preventing possible non read-only operation') if json_request? @@ -57,6 +57,11 @@ module Gitlab !allowlisted_routes end + # Overridden in EE module + def read_only? + Gitlab::Database.read_only? + end + def json_request? APPLICATION_JSON_TYPES.include?(request.media_type) end @@ -97,7 +102,7 @@ module Gitlab return false unless request.post? && request.path.end_with?('.git/git-upload-pack') - ALLOWLISTED_GIT_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) + ALLOWLISTED_GIT_READ_ONLY_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) end def internal_route? diff --git a/lib/gitlab/pagination/gitaly_keyset_pager.rb b/lib/gitlab/pagination/gitaly_keyset_pager.rb index 651e3d5a807..1350168967e 100644 --- a/lib/gitlab/pagination/gitaly_keyset_pager.rb +++ b/lib/gitlab/pagination/gitaly_keyset_pager.rb @@ -15,6 +15,7 @@ module Gitlab # and supports pagination via gitaly. def paginate(finder) return paginate_via_gitaly(finder) if keyset_pagination_enabled? + return paginate_first_page_via_gitaly(finder) if paginate_first_page? branches = ::Kaminari.paginate_array(finder.execute) Gitlab::Pagination::OffsetPagination @@ -25,7 +26,11 @@ module Gitlab private def keyset_pagination_enabled? - Feature.enabled?(:branch_list_keyset_pagination, project) && params[:pagination] == 'keyset' + Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: true) && params[:pagination] == 'keyset' + end + + def paginate_first_page? + Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: true) && (params[:page].blank? || params[:page].to_i == 1) end def paginate_via_gitaly(finder) @@ -34,6 +39,20 @@ module Gitlab end end + # When first page is requested, we paginate the data via Gitaly + # Headers are added to immitate offset pagination, while it is the default option + def paginate_first_page_via_gitaly(finder) + finder.execute(gitaly_pagination: true).tap do |records| + total = project.repository.branch_count + per_page = params[:per_page].presence || Kaminari.config.default_per_page + + Gitlab::Pagination::OffsetHeaderBuilder.new( + request_context: request_context, per_page: per_page, page: 1, next_page: 2, + total: total, total_pages: total / per_page + 1 + ).execute + end + end + def apply_headers(records) if records.count == params[:per_page] Gitlab::Pagination::Keyset::HeaderBuilder diff --git a/lib/gitlab/pagination/offset_header_builder.rb b/lib/gitlab/pagination/offset_header_builder.rb new file mode 100644 index 00000000000..32089e40932 --- /dev/null +++ b/lib/gitlab/pagination/offset_header_builder.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Gitlab + module Pagination + class OffsetHeaderBuilder + attr_reader :request_context, :per_page, :page, :next_page, :prev_page, :total, :total_pages + + delegate :params, :header, :request, to: :request_context + + def initialize(request_context:, per_page:, page:, next_page:, prev_page: nil, total:, total_pages:) + @request_context = request_context + @per_page = per_page + @page = page + @next_page = next_page + @prev_page = prev_page + @total = total + @total_pages = total_pages + end + + def execute(exclude_total_headers: false, data_without_counts: false) + header 'X-Per-Page', per_page.to_s + header 'X-Page', page.to_s + header 'X-Next-Page', next_page.to_s + header 'X-Prev-Page', prev_page.to_s + header 'Link', pagination_links(data_without_counts) + + return if exclude_total_headers || data_without_counts + + header 'X-Total', total.to_s + header 'X-Total-Pages', total_pages.to_s + end + + private + + def pagination_links(data_without_counts) + [].tap do |links| + links << %(<#{page_href(page: prev_page)}>; rel="prev") if prev_page + links << %(<#{page_href(page: next_page)}>; rel="next") if next_page + links << %(<#{page_href(page: 1)}>; rel="first") + + links << %(<#{page_href(page: total_pages)}>; rel="last") unless data_without_counts + end.join(', ') + end + + def base_request_uri + @base_request_uri ||= URI.parse(request.url).tap do |uri| + uri.host = Gitlab.config.gitlab.host + uri.port = Gitlab.config.gitlab.port + end + end + + def build_page_url(query_params:) + base_request_uri.tap do |uri| + uri.query = query_params + end.to_s + end + + def page_href(next_page_params = {}) + query_params = params.merge(**next_page_params, per_page: params[:per_page]).to_query + + build_page_url(query_params: query_params) + end + end + end +end diff --git a/lib/gitlab/pagination/offset_pagination.rb b/lib/gitlab/pagination/offset_pagination.rb index 46c74b8fe3c..2805b12d95d 100644 --- a/lib/gitlab/pagination/offset_pagination.rb +++ b/lib/gitlab/pagination/offset_pagination.rb @@ -48,58 +48,26 @@ module Gitlab end def add_pagination_headers(paginated_data, exclude_total_headers) - header 'X-Per-Page', paginated_data.limit_value.to_s - header 'X-Page', paginated_data.current_page.to_s - header 'X-Next-Page', paginated_data.next_page.to_s - header 'X-Prev-Page', paginated_data.prev_page.to_s - header 'Link', pagination_links(paginated_data) - - return if exclude_total_headers || data_without_counts?(paginated_data) - - header 'X-Total', paginated_data.total_count.to_s - header 'X-Total-Pages', total_pages(paginated_data).to_s - end - - def pagination_links(paginated_data) - [].tap do |links| - links << %(<#{page_href(page: paginated_data.prev_page)}>; rel="prev") if paginated_data.prev_page - links << %(<#{page_href(page: paginated_data.next_page)}>; rel="next") if paginated_data.next_page - links << %(<#{page_href(page: 1)}>; rel="first") - - links << %(<#{page_href(page: total_pages(paginated_data))}>; rel="last") unless data_without_counts?(paginated_data) - end.join(', ') - end - - def total_pages(paginated_data) - # Ensure there is in total at least 1 page - [paginated_data.total_pages, 1].max + Gitlab::Pagination::OffsetHeaderBuilder.new( + request_context: self, per_page: paginated_data.limit_value, page: paginated_data.current_page, + next_page: paginated_data.next_page, prev_page: paginated_data.prev_page, + total: total_count(paginated_data), total_pages: total_pages(paginated_data) + ).execute(exclude_total_headers: exclude_total_headers, data_without_counts: data_without_counts?(paginated_data)) end def data_without_counts?(paginated_data) paginated_data.is_a?(Kaminari::PaginatableWithoutCount) end - def base_request_uri - @base_request_uri ||= URI.parse(request.url).tap do |uri| - uri.host = Gitlab.config.gitlab.host - uri.port = Gitlab.config.gitlab.port - end + def total_count(paginated_data) + paginated_data.total_count unless data_without_counts?(paginated_data) end - def build_page_url(query_params:) - base_request_uri.tap do |uri| - uri.query = query_params - end.to_s - end - - def page_href(next_page_params = {}) - query_params = params.merge(**next_page_params, per_page: per_page).to_query - - build_page_url(query_params: query_params) - end + def total_pages(paginated_data) + return if data_without_counts?(paginated_data) - def per_page - @per_page ||= params[:per_page] + # Ensure there is in total at least 1 page + [paginated_data.total_pages, 1].max end end end diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index ad0a5c80604..2ff23980ebd 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -180,12 +180,16 @@ module Gitlab end end - def project_git_route_regex - @project_git_route_regex ||= /#{project_route_regex}\.git/.freeze + def repository_route_regex + @repository_route_regex ||= /#{full_namespace_route_regex}|#{personal_snippet_repository_path_regex}/.freeze end - def project_wiki_git_route_regex - @project_wiki_git_route_regex ||= /#{PATH_REGEX_STR}\.wiki/.freeze + def repository_git_route_regex + @repository_git_route_regex ||= /#{repository_route_regex}\.git/.freeze + end + + def repository_wiki_git_route_regex + @repository_wiki_git_route_regex ||= /#{full_namespace_route_regex}\.wiki\.git/.freeze end def full_namespace_path_regex @@ -250,10 +254,6 @@ module Gitlab %r{\A(#{personal_snippet_repository_path_regex}|#{project_snippet_repository_path_regex})\z} end - def personal_and_project_snippets_path_regex - %r{#{personal_snippet_path_regex}|#{project_snippet_path_regex}} - end - def container_image_regex @container_image_regex ||= %r{([\w\.-]+\/){0,1}[\w\.-]+}.freeze end diff --git a/lib/gitlab/performance_bar/logger.rb b/lib/gitlab/performance_bar/logger.rb new file mode 100644 index 00000000000..a8e2f7d2d4e --- /dev/null +++ b/lib/gitlab/performance_bar/logger.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Gitlab + module PerformanceBar + class Logger < ::Gitlab::JsonLogger + def self.file_name_noext + 'performance_bar_json' + end + end + end +end diff --git a/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb b/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb index 805283b0f93..bf8d4b202b6 100644 --- a/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb +++ b/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb @@ -5,7 +5,33 @@ module Gitlab module PerformanceBar module RedisAdapterWhenPeekEnabled def save(request_id) - super if ::Gitlab::PerformanceBar.enabled_for_request? + return unless ::Gitlab::PerformanceBar.enabled_for_request? + return if request_id.blank? + + super + + enqueue_stats_job(request_id) + end + + # schedules a job which parses peek profile data and adds them + # to a structured log + def enqueue_stats_job(request_id) + return unless gather_stats? + + @client.sadd(GitlabPerformanceBarStatsWorker::STATS_KEY, request_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables + + return unless uuid = Gitlab::ExclusiveLease.new( + GitlabPerformanceBarStatsWorker::LEASE_KEY, + timeout: GitlabPerformanceBarStatsWorker::LEASE_TIMEOUT + ).try_obtain + + GitlabPerformanceBarStatsWorker.perform_in(GitlabPerformanceBarStatsWorker::WORKER_DELAY, uuid) + end + + def gather_stats? + return unless Feature.enabled?(:performance_bar_stats) + + Gitlab.com? || !Rails.env.production? end end end diff --git a/lib/gitlab/performance_bar/stats.rb b/lib/gitlab/performance_bar/stats.rb new file mode 100644 index 00000000000..d1504d88315 --- /dev/null +++ b/lib/gitlab/performance_bar/stats.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Gitlab + module PerformanceBar + # This class fetches Peek stats stored in redis and logs them in a + # structured log (so these can be then analyzed in Kibana) + class Stats + def initialize(redis) + @redis = redis + end + + def process(id) + data = request(id) + return unless data + + log_sql_queries(id, data) + rescue => err + logger.error(message: "failed to process request id #{id}: #{err.message}") + end + + private + + def request(id) + # Peek gem stores request data under peek:requests:request_id key + json_data = @redis.get("peek:requests:#{id}") + Gitlab::Json.parse(json_data) + end + + def log_sql_queries(id, data) + return [] unless queries = data.dig('data', 'active-record', 'details') + + queries.each do |query| + next unless location = parse_backtrace(query['backtrace']) + + log_info = location.merge( + type: :sql, + request_id: id, + duration_ms: query['duration'].to_f + ) + + logger.info(log_info) + end + end + + def parse_backtrace(backtrace) + return unless match = /(?<filename>.*):(?<filenum>\d+):in `(?<method>.*)'/.match(backtrace.first) + + { + filename: match[:filename], + filenum: match[:filenum].to_i, + method: match[:method] + } + end + + def logger + @logger ||= Gitlab::PerformanceBar::Logger.build + end + end + end +end diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb index a830f949b21..6ba36fadfa3 100644 --- a/lib/gitlab/project_template.rb +++ b/lib/gitlab/project_template.rb @@ -40,30 +40,30 @@ module Gitlab # TODO: Review child inheritance of this table (see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430928221) def localized_templates_table [ - ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'), - ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'), - ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'), - ProjectTemplate.new('iosswift', 'iOS (Swift)', _('A ready-to-go template for use with iOS Swift apps.'), 'https://gitlab.com/gitlab-org/project-templates/iosswift', 'illustrations/logos/swift.svg'), + ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'), + ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'), + ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'), + ProjectTemplate.new('iosswift', 'iOS (Swift)', _('A ready-to-go template for use with iOS Swift apps'), 'https://gitlab.com/gitlab-org/project-templates/iosswift', 'illustrations/logos/swift.svg'), ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/logos/dotnet.svg'), - ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps.'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'), - ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development.'), 'https://gitlab.com/gitlab-org/project-templates/go-micro', 'illustrations/logos/gomicro.svg'), - ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby.'), 'https://gitlab.com/pages/gatsby'), - ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo.'), 'https://gitlab.com/pages/hugo', 'illustrations/logos/hugo.svg'), - ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll.'), 'https://gitlab.com/pages/jekyll', 'illustrations/logos/jekyll.svg'), - ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML.'), 'https://gitlab.com/pages/plain-html'), - ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook.'), 'https://gitlab.com/pages/gitbook', 'illustrations/logos/gitbook.svg'), - ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo.'), 'https://gitlab.com/pages/hexo', 'illustrations/logos/hexo.svg'), + ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'), + ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development'), 'https://gitlab.com/gitlab-org/project-templates/go-micro', 'illustrations/logos/gomicro.svg'), + ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby'), 'https://gitlab.com/pages/gatsby'), + ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo'), 'https://gitlab.com/pages/hugo', 'illustrations/logos/hugo.svg'), + ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll'), 'https://gitlab.com/pages/jekyll', 'illustrations/logos/jekyll.svg'), + ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML'), 'https://gitlab.com/pages/plain-html'), + ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook'), 'https://gitlab.com/pages/gitbook', 'illustrations/logos/gitbook.svg'), + ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo'), 'https://gitlab.com/pages/hexo', 'illustrations/logos/hexo.svg'), ProjectTemplate.new('sse_middleman', 'Static Site Editor/Middleman', _('Middleman project with Static Site Editor support'), 'https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman', 'illustrations/logos/middleman.svg'), ProjectTemplate.new('gitpod_spring_petclinic', 'Gitpod/Spring Petclinic', _('A Gitpod configured Webapplication in Spring and Java'), 'https://gitlab.com/gitlab-org/project-templates/gitpod-spring-petclinic', 'illustrations/logos/gitpod.svg'), - ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'), + ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'), ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg'), ProjectTemplate.new('jsonnet', 'Jsonnet for Dynamic Child Pipelines', _('An example showing how to use Jsonnet with GitLab dynamic child pipelines'), 'https://gitlab.com/gitlab-org/project-templates/jsonnet'), - ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab.'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management') + ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management') ].freeze end diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb index b0aae363749..1822b0c8bd5 100644 --- a/lib/gitlab/quick_actions/issue_actions.rb +++ b/lib/gitlab/quick_actions/issue_actions.rb @@ -102,6 +102,41 @@ module Gitlab @execution_message[:duplicate] = message end + desc _('Clone this issue') + explanation do |project = quick_action_target.project.full_path| + _("Clones this issue, without comments, to %{project}.") % { project: project } + end + params 'path/to/project [--with_notes]' + types Issue + condition do + quick_action_target.persisted? && + current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) + end + command :clone do |params = ''| + params = params.split(' ') + with_notes = params.delete('--with_notes').present? + + # If we have more than 1 param, then the user supplied too many spaces, or mistyped `--with_notes` + if params.size > 1 + @execution_message[:clone] = _('Failed to clone this issue: wrong parameters.') + next + end + + target_project_path = params[0] + target_project = target_project_path.present? ? Project.find_by_full_path(target_project_path) : quick_action_target.project + + if target_project.present? + @updates[:target_clone_project] = target_project + @updates[:clone_with_notes] = with_notes + + message = _("Cloned this issue to %{path_to_project}.") % { path_to_project: target_project_path || quick_action_target.project.full_path } + else + message = _("Failed to clone this issue because target project doesn't exist.") + end + + @execution_message[:clone] = message + end + desc _('Move this issue to another project.') explanation do |path_to_project| _("Moves this issue to %{path_to_project}.") % { path_to_project: path_to_project } diff --git a/lib/gitlab/rack_attack.rb b/lib/gitlab/rack_attack.rb new file mode 100644 index 00000000000..7c336153e32 --- /dev/null +++ b/lib/gitlab/rack_attack.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +# When adding new user-configurable throttles, remember to update the documentation +# in doc/user/admin_area/settings/user_and_ip_rate_limits.md +# +# Integration specs for throttling can be found in: +# spec/requests/rack_attack_global_spec.rb +module Gitlab + module RackAttack + def self.configure(rack_attack) + # This adds some methods used by our throttles to the `Rack::Request` + rack_attack::Request.include(Gitlab::RackAttack::Request) + # Send the Retry-After header so clients (e.g. python-gitlab) can make good choices about delays + Rack::Attack.throttled_response_retry_after_header = true + # Configure the throttles + configure_throttles(rack_attack) + + configure_user_allowlist + end + + def self.configure_user_allowlist + @user_allowlist = nil + user_allowlist + end + + def self.configure_throttles(rack_attack) + throttle_or_track(rack_attack, 'throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req| + if !req.should_be_skipped? && + Gitlab::Throttle.settings.throttle_unauthenticated_enabled && + req.unauthenticated? + req.ip + end + end + + throttle_or_track(rack_attack, 'throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req| + if req.api_request? && + Gitlab::Throttle.settings.throttle_authenticated_api_enabled + req.throttled_user_id([:api]) + end + end + + # Product analytics feature is in experimental stage. + # At this point we want to limit amount of events registered + # per application (aid stands for application id). + throttle_or_track(rack_attack, 'throttle_product_analytics_collector', limit: 100, period: 60) do |req| + if req.product_analytics_collector_request? + req.params['aid'] + end + end + + throttle_or_track(rack_attack, 'throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req| + if req.web_request? && + Gitlab::Throttle.settings.throttle_authenticated_web_enabled + req.throttled_user_id([:api, :rss, :ics]) + end + end + + throttle_or_track(rack_attack, 'throttle_unauthenticated_protected_paths', Gitlab::Throttle.protected_paths_options) do |req| + if req.post? && + !req.should_be_skipped? && + req.protected_path? && + Gitlab::Throttle.protected_paths_enabled? && + req.unauthenticated? + req.ip + end + end + + throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_api', Gitlab::Throttle.protected_paths_options) do |req| + if req.post? && + req.api_request? && + req.protected_path? && + Gitlab::Throttle.protected_paths_enabled? + req.throttled_user_id([:api]) + end + end + + throttle_or_track(rack_attack, 'throttle_authenticated_protected_paths_web', Gitlab::Throttle.protected_paths_options) do |req| + if req.post? && + req.web_request? && + req.protected_path? && + Gitlab::Throttle.protected_paths_enabled? + req.throttled_user_id([:api, :rss, :ics]) + end + end + + rack_attack.safelist('throttle_bypass_header') do |req| + Gitlab::Throttle.bypass_header.present? && + req.get_header(Gitlab::Throttle.bypass_header) == '1' + end + end + + def self.throttle_or_track(rack_attack, throttle_name, *args, &block) + if track?(throttle_name) + rack_attack.track(throttle_name, *args, &block) + else + rack_attack.throttle(throttle_name, *args, &block) + end + end + + def self.track?(name) + dry_run_config = ENV['GITLAB_THROTTLE_DRY_RUN'].to_s.strip + + return false if dry_run_config.empty? + return true if dry_run_config == '*' + + dry_run_config.split(',').map(&:strip).include?(name) + end + + def self.user_allowlist + @user_allowlist ||= begin + list = UserAllowlist.new(ENV['GITLAB_THROTTLE_USER_ALLOWLIST']) + Gitlab::AuthLogger.info(gitlab_throttle_user_allowlist: list.to_a) + list + end + end + end +end +::Gitlab::RackAttack.prepend_if_ee('::EE::Gitlab::RackAttack') diff --git a/lib/gitlab/rack_attack/request.rb b/lib/gitlab/rack_attack/request.rb new file mode 100644 index 00000000000..67e3a5de223 --- /dev/null +++ b/lib/gitlab/rack_attack/request.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Gitlab + module RackAttack + module Request + def unauthenticated? + !(authenticated_user_id([:api, :rss, :ics]) || authenticated_runner_id) + end + + def throttled_user_id(request_formats) + user_id = authenticated_user_id(request_formats) + + if Gitlab::RackAttack.user_allowlist.include?(user_id) + Gitlab::Instrumentation::Throttle.safelist = 'throttle_user_allowlist' + return + end + + user_id + end + + def authenticated_runner_id + request_authenticator.runner&.id + end + + def api_request? + path.start_with?('/api') + end + + def api_internal_request? + path =~ %r{^/api/v\d+/internal/} + end + + def health_check_request? + path =~ %r{^/-/(health|liveness|readiness|metrics)} + end + + def product_analytics_collector_request? + path.start_with?('/-/collector/i') + end + + def should_be_skipped? + api_internal_request? || health_check_request? + end + + def web_request? + !api_request? && !health_check_request? + end + + def protected_path? + !protected_path_regex.nil? + end + + def protected_path_regex + path =~ protected_paths_regex + end + + private + + def authenticated_user_id(request_formats) + request_authenticator.user(request_formats)&.id + end + + def request_authenticator + @request_authenticator ||= Gitlab::Auth::RequestAuthenticator.new(self) + end + + def protected_paths + Gitlab::CurrentSettings.current_application_settings.protected_paths + end + + def protected_paths_regex + Regexp.union(protected_paths.map { |path| /\A#{Regexp.escape(path)}/ }) + end + end + end +end +::Gitlab::RackAttack::Request.prepend_if_ee('::EE::Gitlab::RackAttack::Request') diff --git a/lib/gitlab/rack_attack/user_allowlist.rb b/lib/gitlab/rack_attack/user_allowlist.rb new file mode 100644 index 00000000000..f3043f44091 --- /dev/null +++ b/lib/gitlab/rack_attack/user_allowlist.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'set' + +module Gitlab + module RackAttack + class UserAllowlist + extend Forwardable + + def_delegators :@set, :empty?, :include?, :to_a + + def initialize(list) + @set = Set.new + + list.to_s.split(',').each do |id| + @set << Integer(id) unless id.blank? + rescue ArgumentError + Gitlab::AuthLogger.error(message: 'ignoring invalid user allowlist entry', entry: id) + end + end + end + end +end diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb index 9ee6f67e455..46c84107e0f 100644 --- a/lib/gitlab/repo_path.rb +++ b/lib/gitlab/repo_path.rb @@ -4,8 +4,15 @@ module Gitlab module RepoPath NotFoundError = Class.new(StandardError) + # Returns an array containing: + # - The repository container + # - The related project (if available) + # - The repository type + # - The original container path (if redirected) + # + # @returns [HasRepository, Project, String, String] def self.parse(path) - repo_path = path.sub(/\.git\z/, '').sub(%r{\A/}, '') + repo_path = path.delete_prefix('/').delete_suffix('.git') redirected_path = nil # Detect the repo type based on the path, the first one tried is the project @@ -30,7 +37,15 @@ module Gitlab [nil, nil, Gitlab::GlRepository.default_type, nil] end + # Returns an array containing: + # - The repository container + # - The related project (if available) + # - The original container path (if redirected) + # + # @returns [HasRepository, Project, String] def self.find_container(type, full_path) + return [nil, nil, nil] if full_path.blank? + if type.snippet? snippet, redirected_path = find_snippet(full_path) @@ -47,26 +62,24 @@ module Gitlab end def self.find_project(project_path) - return [nil, nil] if project_path.blank? - project = Project.find_by_full_path(project_path, follow_redirects: true) - redirected_path = redirected?(project, project_path) ? project_path : nil + redirected_path = project_path if redirected?(project, project_path) [project, redirected_path] end - def self.redirected?(project, project_path) - project && project.full_path.casecmp(project_path) != 0 + def self.redirected?(container, container_path) + container && container.full_path.casecmp(container_path) != 0 end # Snippet_path can be either: # - snippets/1 # - h5bp/html5-boilerplate/snippets/53 def self.find_snippet(snippet_path) - return [nil, nil] if snippet_path.blank? - snippet_id, project_path = extract_snippet_info(snippet_path) - project, redirected_path = find_project(project_path) + return [nil, nil] unless snippet_id + + project, redirected_path = find_project(project_path) if project_path [Snippet.find_by_id_and_project(id: snippet_id, project: project), redirected_path] end @@ -74,19 +87,23 @@ module Gitlab # Wiki path can be either: # - namespace/project # - group/subgroup/project - def self.find_wiki(wiki_path) - return [nil, nil] if wiki_path.blank? - - project, redirected_path = find_project(wiki_path) - - [project&.wiki, redirected_path] + # + # And also in EE: + # - group + # - group/subgroup + def self.find_wiki(container_path) + container = Routable.find_by_full_path(container_path, follow_redirects: true) + redirected_path = container_path if redirected?(container, container_path) + + # In CE, Group#wiki is not available so this will return nil for a group path. + [container&.try(:wiki), redirected_path] end def self.extract_snippet_info(snippet_path) path_segments = snippet_path.split('/') snippet_id = path_segments.pop - path_segments.pop # Remove snippets from path - project_path = File.join(path_segments) + path_segments.pop # Remove 'snippets' from path + project_path = File.join(path_segments).presence [snippet_id, project_path] end diff --git a/lib/gitlab/request_forgery_protection.rb b/lib/gitlab/request_forgery_protection.rb index b1e478093d3..79562a8223b 100644 --- a/lib/gitlab/request_forgery_protection.rb +++ b/lib/gitlab/request_forgery_protection.rb @@ -9,14 +9,6 @@ module Gitlab class Controller < ActionController::Base protect_from_forgery with: :exception, prepend: true - rescue_from ActionController::InvalidAuthenticityToken do |e| - logger.warn "This CSRF token verification failure is handled internally by `GitLab::RequestForgeryProtection`" - logger.warn "Unlike the logs may suggest, this does not result in an actual 422 response to the user" - logger.warn "For API requests, the only effect is that `current_user` will be `nil` for the duration of the request" - - raise e - end - def index head :ok end diff --git a/lib/gitlab/sample_data_template.rb b/lib/gitlab/sample_data_template.rb index ae74dc710b7..06ea53e4018 100644 --- a/lib/gitlab/sample_data_template.rb +++ b/lib/gitlab/sample_data_template.rb @@ -5,8 +5,7 @@ module Gitlab class << self def localized_templates_table [ - SampleDataTemplate.new('basic', 'Basic', _('Basic Sample Data template with Issues, Merge Requests and Milestones.'), 'https://gitlab.com/gitlab-org/sample-data-templates/basic'), - SampleDataTemplate.new('serenity_valley', 'Serenity Valley', _('Serenity Valley Sample Data template.'), 'https://gitlab.com/gitlab-org/sample-data-templates/serenity-valley') + SampleDataTemplate.new('sample', 'Sample GitLab Project', _('Get started with a project that follows best practices for setting up GitLab for your own organization, including sample Issues, Merge Requests, and Milestones'), 'https://gitlab.com/gitlab-org/sample-data-templates/sample-gitlab-project') ].freeze end diff --git a/lib/gitlab/sanitizers/exif.rb b/lib/gitlab/sanitizers/exif.rb index 78c517c49d8..ed3e32f3e79 100644 --- a/lib/gitlab/sanitizers/exif.rb +++ b/lib/gitlab/sanitizers/exif.rb @@ -67,7 +67,7 @@ module Gitlab batch_size: 1000 } - relation.find_each(find_params) do |upload| + relation.find_each(**find_params) do |upload| clean(upload.retrieve_uploader, dry_run: dry_run) sleep sleep_time if sleep_time rescue => err diff --git a/lib/gitlab/search/query.rb b/lib/gitlab/search/query.rb index 27ea0b7367f..5b1f9400bc7 100644 --- a/lib/gitlab/search/query.rb +++ b/lib/gitlab/search/query.rb @@ -51,6 +51,7 @@ module Gitlab end query = (@raw_query.split - fragments).join(' ') + query = '*' if query.empty? [query, filters] end diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb index 259d3e300b6..48f204e0b86 100644 --- a/lib/gitlab/setup_helper.rb +++ b/lib/gitlab/setup_helper.rb @@ -4,10 +4,10 @@ require 'toml-rb' module Gitlab module SetupHelper - def create_configuration(dir, storage_paths, force: false) + def create_configuration(dir, storage_paths, force: false, options: {}) generate_configuration( - configuration_toml(dir, storage_paths), - get_config_path(dir), + configuration_toml(dir, storage_paths, options), + get_config_path(dir, options), force: force ) end @@ -31,7 +31,7 @@ module Gitlab module Workhorse extend Gitlab::SetupHelper class << self - def configuration_toml(dir, _) + def configuration_toml(dir, _, _) config = { redis: { URL: redis_url } } TomlRB.dump(config) @@ -41,8 +41,26 @@ module Gitlab Gitlab::Redis::SharedState.url end - def get_config_path(dir) - File.join(dir, 'config.toml') + def get_config_path(dir, _) + File.join(dir, 'config_path') + end + + def compile_into(dir) + command = %W[#{make} -C #{Rails.root.join('workhorse')} install PREFIX=#{File.absolute_path(dir)}] + + make_out, make_status = Gitlab::Popen.popen(command) + unless make_status == 0 + warn make_out + raise 'workhorse make failed' + end + + # 'make install' puts the binaries in #{dir}/bin but the init script expects them in dir + FileUtils.mv(Dir["#{dir}/bin/*"], dir) + end + + def make + _, which_status = Gitlab::Popen.popen(%w[which gmake]) + which_status == 0 ? 'gmake' : 'make' end end end @@ -58,7 +76,7 @@ module Gitlab # because it uses a Unix socket. # For development and testing purposes, an extra storage is added to gitaly, # which is not known to Rails, but must be explicitly stubbed. - def configuration_toml(gitaly_dir, storage_paths, gitaly_ruby: true) + def configuration_toml(gitaly_dir, storage_paths, options, gitaly_ruby: true) storages = [] address = nil @@ -79,14 +97,20 @@ module Gitlab config = { socket_path: address.sub(/\Aunix:/, '') } if Rails.env.test? + socket_filename = options[:gitaly_socket] || "gitaly.socket" + + config = { + # Override the set gitaly_address since Praefect is in the loop + socket_path: File.join(gitaly_dir, socket_filename), + auth: { token: 'secret' }, + # Compared to production, tests run in constrained environments. This + # number is meant to grow with the number of concurrent rails requests / + # sidekiq jobs, and concurrency will be low anyway in test. + git: { catfile_cache_size: 5 } + } + storage_path = Rails.root.join('tmp', 'tests', 'second_storage').to_s storages << { name: 'test_second_storage', path: storage_path } - - config[:auth] = { token: 'secret' } - # Compared to production, tests run in constrained environments. This - # number is meant to grow with the number of concurrent rails requests / - # sidekiq jobs, and concurrency will be low anyway in test. - config[:git] = { catfile_cache_size: 5 } end config[:storage] = storages @@ -106,8 +130,9 @@ module Gitlab private - def get_config_path(dir) - File.join(dir, 'config.toml') + def get_config_path(dir, options) + config_filename = options[:config_filename] || 'config.toml' + File.join(dir, config_filename) end end end @@ -115,9 +140,11 @@ module Gitlab module Praefect extend Gitlab::SetupHelper class << self - def configuration_toml(gitaly_dir, storage_paths) + def configuration_toml(gitaly_dir, _, _) nodes = [{ storage: 'default', address: "unix:#{gitaly_dir}/gitaly.socket", primary: true, token: 'secret' }] - storages = [{ name: 'default', node: nodes }] + second_storage_nodes = [{ storage: 'test_second_storage', address: "unix:#{gitaly_dir}/gitaly2.socket", primary: true, token: 'secret' }] + + storages = [{ name: 'default', node: nodes }, { name: 'test_second_storage', node: second_storage_nodes }] failover = { enabled: false } config = { socket_path: "#{gitaly_dir}/praefect.socket", memory_queue_enabled: true, virtual_storage: storages, failover: failover } config[:token] = 'secret' if Rails.env.test? @@ -127,7 +154,7 @@ module Gitlab private - def get_config_path(dir) + def get_config_path(dir, _) File.join(dir, 'praefect.config.toml') end end diff --git a/lib/gitlab/sidekiq_cluster.rb b/lib/gitlab/sidekiq_cluster.rb index d05c717d2fa..cc1bd282da8 100644 --- a/lib/gitlab/sidekiq_cluster.rb +++ b/lib/gitlab/sidekiq_cluster.rb @@ -111,7 +111,7 @@ module Gitlab end def self.count_by_queue(queues) - queues.each_with_object(Hash.new(0)) { |element, hash| hash[element] += 1 } + queues.tally end def self.proc_details(counts) diff --git a/lib/gitlab/sidekiq_death_handler.rb b/lib/gitlab/sidekiq_death_handler.rb new file mode 100644 index 00000000000..f86d9f17b5f --- /dev/null +++ b/lib/gitlab/sidekiq_death_handler.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqDeathHandler + class << self + include ::Gitlab::SidekiqMiddleware::MetricsHelper + + def handler(job, _exception) + labels = create_labels(job['class'].constantize, job['queue']) + + counter.increment(labels) + end + + def counter + @counter ||= ::Gitlab::Metrics.counter(:sidekiq_jobs_dead_total, 'Sidekiq dead jobs') + end + end + end +end diff --git a/lib/gitlab/sidekiq_middleware/client_metrics.rb b/lib/gitlab/sidekiq_middleware/client_metrics.rb index 245a1b5e024..7ee8a623d30 100644 --- a/lib/gitlab/sidekiq_middleware/client_metrics.rb +++ b/lib/gitlab/sidekiq_middleware/client_metrics.rb @@ -2,7 +2,9 @@ module Gitlab module SidekiqMiddleware - class ClientMetrics < SidekiqMiddleware::Metrics + class ClientMetrics + include ::Gitlab::SidekiqMiddleware::MetricsHelper + ENQUEUED = :sidekiq_enqueued_jobs_total def initialize diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb index 5efd1b34d32..79ac853ea0c 100644 --- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb +++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb @@ -70,10 +70,6 @@ module Gitlab jid != existing_jid end - def droppable? - idempotent? && ::Feature.disabled?("disable_#{queue_name}_deduplication", type: :ops) - end - def scheduled_at job['at'] end @@ -85,6 +81,13 @@ module Gitlab worker_klass.get_deduplication_options end + def idempotent? + return false unless worker_klass + return false unless worker_klass.respond_to?(:idempotent?) + + worker_klass.idempotent? + end + private attr_reader :queue_name, :job @@ -128,13 +131,6 @@ module Gitlab def idempotency_string "#{worker_class_name}:#{arguments.join('-')}" end - - def idempotent? - return false unless worker_klass - return false unless worker_klass.respond_to?(:idempotent?) - - worker_klass.idempotent? - end end end end diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb index 59b0e7e29da..469033a5e52 100644 --- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb +++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/deduplicates_when_scheduling.rb @@ -13,7 +13,7 @@ module Gitlab if deduplicatable_job? && check! && duplicate_job.duplicate? job['duplicate-of'] = duplicate_job.existing_jid - if duplicate_job.droppable? + if duplicate_job.idempotent? Gitlab::SidekiqLogging::DeduplicationLogger.instance.log( job, "dropped #{strategy_name}", duplicate_job.options) return false diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics_helper.rb index 7ae8995c46d..5c1ce2b98e8 100644 --- a/lib/gitlab/sidekiq_middleware/metrics.rb +++ b/lib/gitlab/sidekiq_middleware/metrics_helper.rb @@ -2,7 +2,7 @@ module Gitlab module SidekiqMiddleware - class Metrics + module MetricsHelper TRUE_LABEL = "yes" FALSE_LABEL = "no" diff --git a/lib/gitlab/sidekiq_middleware/server_metrics.rb b/lib/gitlab/sidekiq_middleware/server_metrics.rb index 0635c07ae4b..7f3048f4c6e 100644 --- a/lib/gitlab/sidekiq_middleware/server_metrics.rb +++ b/lib/gitlab/sidekiq_middleware/server_metrics.rb @@ -2,7 +2,9 @@ module Gitlab module SidekiqMiddleware - class ServerMetrics < SidekiqMiddleware::Metrics + class ServerMetrics + include ::Gitlab::SidekiqMiddleware::MetricsHelper + # SIDEKIQ_LATENCY_BUCKETS are latency histogram buckets better suited to Sidekiq # timeframes than the DEFAULT_BUCKET definition. Defined in seconds. SIDEKIQ_LATENCY_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60, 300, 600].freeze diff --git a/lib/gitlab/throttle.rb b/lib/gitlab/throttle.rb new file mode 100644 index 00000000000..aebf8d92cb3 --- /dev/null +++ b/lib/gitlab/throttle.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Gitlab + class Throttle + def self.settings + Gitlab::CurrentSettings.current_application_settings + end + + # Returns true if we should use the Admin Area protected paths throttle + def self.protected_paths_enabled? + self.settings.throttle_protected_paths_enabled? + end + + def self.omnibus_protected_paths_present? + Rack::Attack.throttles.key?('protected paths') + end + + def self.bypass_header + env_value = ENV['GITLAB_THROTTLE_BYPASS_HEADER'] + return unless env_value.present? + + "HTTP_#{env_value.upcase.tr('-', '_')}" + end + + def self.unauthenticated_options + limit_proc = proc { |req| settings.throttle_unauthenticated_requests_per_period } + period_proc = proc { |req| settings.throttle_unauthenticated_period_in_seconds.seconds } + { limit: limit_proc, period: period_proc } + end + + def self.authenticated_api_options + limit_proc = proc { |req| settings.throttle_authenticated_api_requests_per_period } + period_proc = proc { |req| settings.throttle_authenticated_api_period_in_seconds.seconds } + { limit: limit_proc, period: period_proc } + end + + def self.authenticated_web_options + limit_proc = proc { |req| settings.throttle_authenticated_web_requests_per_period } + period_proc = proc { |req| settings.throttle_authenticated_web_period_in_seconds.seconds } + { limit: limit_proc, period: period_proc } + end + + def self.protected_paths_options + limit_proc = proc { |req| settings.throttle_protected_paths_requests_per_period } + period_proc = proc { |req| settings.throttle_protected_paths_period_in_seconds.seconds } + + { limit: limit_proc, period: period_proc } + end + end +end diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb index 19be468e3d5..618e359211b 100644 --- a/lib/gitlab/tracking.rb +++ b/lib/gitlab/tracking.rb @@ -14,8 +14,8 @@ module Gitlab Gitlab::Tracking.event(category, action.to_s, **args) end - def track_self_describing_event(schema_url, event_data_json, **args) - Gitlab::Tracking.self_describing_event(schema_url, event_data_json, **args) + def track_self_describing_event(schema_url, data:, **args) + Gitlab::Tracking.self_describing_event(schema_url, data: data, **args) end end @@ -26,10 +26,11 @@ module Gitlab def event(category, action, label: nil, property: nil, value: nil, context: nil) snowplow.event(category, action, label: label, property: property, value: value, context: context) + product_analytics.event(category, action, label: label, property: property, value: value, context: context) end - def self_describing_event(schema_url, event_data_json, context: nil) - snowplow.self_describing_event(schema_url, event_data_json, context: context) + def self_describing_event(schema_url, data:, context: nil) + snowplow.self_describing_event(schema_url, data: data, context: context) end def snowplow_options(group) @@ -49,6 +50,10 @@ module Gitlab def snowplow @snowplow ||= Gitlab::Tracking::Destinations::Snowplow.new end + + def product_analytics + @product_analytics ||= Gitlab::Tracking::Destinations::ProductAnalytics.new + end end end end diff --git a/lib/gitlab/tracking/destinations/product_analytics.rb b/lib/gitlab/tracking/destinations/product_analytics.rb new file mode 100644 index 00000000000..cacedbc5b83 --- /dev/null +++ b/lib/gitlab/tracking/destinations/product_analytics.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Gitlab + module Tracking + module Destinations + class ProductAnalytics < Base + extend ::Gitlab::Utils::Override + include ::Gitlab::Utils::StrongMemoize + + override :event + def event(category, action, label: nil, property: nil, value: nil, context: nil) + return unless event_allowed?(category, action) + return unless enabled? + + tracker.track_struct_event(category, action, label, property, value, context, (Time.now.to_f * 1000).to_i) + end + + private + + def event_allowed?(category, action) + category == 'epics' && action == 'promote' + end + + def enabled? + Feature.enabled?(:product_analytics_tracking, type: :ops) && + Gitlab::CurrentSettings.usage_ping_enabled? && + Gitlab::CurrentSettings.self_monitoring_project_id.present? + end + + def tracker + @tracker ||= SnowplowTracker::Tracker.new( + SnowplowTracker::AsyncEmitter.new(::ProductAnalytics::Tracker::COLLECTOR_URL, protocol: Gitlab.config.gitlab.protocol), + SnowplowTracker::Subject.new, + Gitlab::Tracking::SNOWPLOW_NAMESPACE, + Gitlab::CurrentSettings.self_monitoring_project_id.to_s + ) + end + end + end + end +end diff --git a/lib/gitlab/tracking/destinations/snowplow.rb b/lib/gitlab/tracking/destinations/snowplow.rb index 9cebcfe5ee1..4fa844de325 100644 --- a/lib/gitlab/tracking/destinations/snowplow.rb +++ b/lib/gitlab/tracking/destinations/snowplow.rb @@ -15,10 +15,10 @@ module Gitlab tracker.track_struct_event(category, action, label, property, value, context, (Time.now.to_f * 1000).to_i) end - def self_describing_event(schema_url, event_data_json, context: nil) + def self_describing_event(schema_url, data:, context: nil) return unless enabled? - event_json = SnowplowTracker::SelfDescribingJson.new(schema_url, event_data_json) + event_json = SnowplowTracker::SelfDescribingJson.new(schema_url, data) tracker.track_self_describing_event(event_json, context, (Time.now.to_f * 1000).to_i) end diff --git a/lib/gitlab/uploads/migration_helper.rb b/lib/gitlab/uploads/migration_helper.rb index 9377ccfec1e..b610d2a10c6 100644 --- a/lib/gitlab/uploads/migration_helper.rb +++ b/lib/gitlab/uploads/migration_helper.rb @@ -75,3 +75,5 @@ module Gitlab end end end + +Gitlab::Uploads::MigrationHelper.prepend_if_ee('EE::Gitlab::Uploads::MigrationHelper') diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 4b0dd54683b..f935c677930 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -47,7 +47,6 @@ module Gitlab .merge(system_usage_data_weekly) .merge(features_usage_data) .merge(components_usage_data) - .merge(cycle_analytics_usage_data) .merge(object_store_usage_data) .merge(topology_usage_data) .merge(usage_activity_by_stage) @@ -237,7 +236,9 @@ module Gitlab def system_usage_data_settings { - settings: {} + settings: { + ldap_encrypted_secrets_enabled: alt_usage_data(fallback: nil) { Gitlab::Auth::Ldap::Config.encrypted_secrets.active? } + } } end @@ -250,12 +251,6 @@ module Gitlab } end - def cycle_analytics_usage_data - Gitlab::CycleAnalytics::UsageData.new.to_json - rescue ActiveRecord::StatementInvalid - { avg_cycle_analytics: {} } - end - # rubocop:disable CodeReuse/ActiveRecord def grafana_embed_usage_data count(Issue.joins('JOIN grafana_integrations USING (project_id)') @@ -296,20 +291,7 @@ module Gitlab # @return [Array<#totals>] An array of objects that respond to `#totals` def usage_data_counters - [ - Gitlab::UsageDataCounters::WikiPageCounter, - Gitlab::UsageDataCounters::WebIdeCounter, - Gitlab::UsageDataCounters::NoteCounter, - Gitlab::UsageDataCounters::SnippetCounter, - Gitlab::UsageDataCounters::SearchCounter, - Gitlab::UsageDataCounters::CycleAnalyticsCounter, - Gitlab::UsageDataCounters::ProductivityAnalyticsCounter, - Gitlab::UsageDataCounters::SourceCodeCounter, - Gitlab::UsageDataCounters::MergeRequestCounter, - Gitlab::UsageDataCounters::DesignsCounter, - Gitlab::UsageDataCounters::KubernetesAgentCounter, - Gitlab::UsageDataCounters::StaticSiteEditorCounter - ] + Gitlab::UsageDataCounters.counters end def components_usage_data @@ -602,7 +584,7 @@ module Gitlab gitlab: distinct_count(::BulkImport.where(time_period, source_type: :gitlab), :user_id) }, projects_imported: { - total: count(Project.where(time_period).where.not(import_type: nil)), + total: distinct_count(::Project.where(time_period).where.not(import_type: nil), :creator_id), gitlab_project: projects_imported_count('gitlab_project', time_period), gitlab: projects_imported_count('gitlab', time_period), github: projects_imported_count('github', time_period), @@ -707,16 +689,12 @@ module Gitlab end def aggregated_metrics_monthly - return {} unless Feature.enabled?(:product_analytics_aggregated_metrics) - { aggregated_metrics: ::Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics_monthly_data } end def aggregated_metrics_weekly - return {} unless Feature.enabled?(:product_analytics_aggregated_metrics) - { aggregated_metrics: ::Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics_weekly_data } @@ -783,12 +761,13 @@ module Gitlab action_monthly_active_users_web_ide_edit: redis_usage_data { counter.count_web_ide_edit_actions(**date_range) }, action_monthly_active_users_sfe_edit: redis_usage_data { counter.count_sfe_edit_actions(**date_range) }, action_monthly_active_users_snippet_editor_edit: redis_usage_data { counter.count_snippet_editor_edit_actions(**date_range) }, + action_monthly_active_users_sse_edit: redis_usage_data { counter.count_sse_edit_actions(**date_range) }, action_monthly_active_users_ide_edit: redis_usage_data { counter.count_edit_using_editor(**date_range) } } end def report_snowplow_events? - self_monitoring_project && Feature.enabled?(:product_analytics, self_monitoring_project) + self_monitoring_project && Feature.enabled?(:product_analytics_tracking, type: :ops) end def distinct_count_service_desk_enabled_projects(time_period) @@ -915,7 +894,7 @@ module Gitlab end def projects_imported_count(from, time_period) - distinct_count(::Project.imported_from(from).where(time_period), :creator_id) # rubocop: disable CodeReuse/ActiveRecord + distinct_count(::Project.imported_from(from).where(time_period).where.not(import_type: nil), :creator_id) # rubocop: disable CodeReuse/ActiveRecord end # rubocop:disable CodeReuse/ActiveRecord diff --git a/lib/gitlab/usage_data_counters.rb b/lib/gitlab/usage_data_counters.rb new file mode 100644 index 00000000000..ca7699e64e1 --- /dev/null +++ b/lib/gitlab/usage_data_counters.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module UsageDataCounters + COUNTERS = [ + GuestPackageEventCounter, + WikiPageCounter, + WebIdeCounter, + NoteCounter, + SnippetCounter, + SearchCounter, + CycleAnalyticsCounter, + ProductivityAnalyticsCounter, + SourceCodeCounter, + MergeRequestCounter, + DesignsCounter, + KubernetesAgentCounter, + StaticSiteEditorCounter + ].freeze + + UsageDataCounterError = Class.new(StandardError) + UnknownEvent = Class.new(UsageDataCounterError) + + class << self + def counters + self::COUNTERS + end + + def count(event_name) + counters.each do |counter| + event = counter.fetch_supported_event(event_name) + + return counter.count(event) if event + end + + raise UnknownEvent, "Cannot find counter for event #{event_name}" + end + end + end +end diff --git a/lib/gitlab/usage_data_counters/aggregated_metrics/common.yml b/lib/gitlab/usage_data_counters/aggregated_metrics/common.yml index 97ec8423b95..b7c0abae227 100644 --- a/lib/gitlab/usage_data_counters/aggregated_metrics/common.yml +++ b/lib/gitlab/usage_data_counters/aggregated_metrics/common.yml @@ -11,7 +11,31 @@ - name: product_analytics_test_metrics_union operator: OR events: ['i_search_total', 'i_search_advanced', 'i_search_paid'] - feature_flag: product_analytics_aggregated_metrics - name: product_analytics_test_metrics_intersection operator: AND events: ['i_search_total', 'i_search_advanced', 'i_search_paid'] +- name: incident_management_alerts_total_unique_counts + operator: OR + events: [ + 'incident_management_alert_status_changed', + 'incident_management_alert_assigned', + 'incident_management_alert_todo', + 'incident_management_alert_create_incident' + ] + feature_flag: usage_data_incident_management_alerts_total_unique_counts +- name: incident_management_incidents_total_unique_counts + operator: OR + events: [ + 'incident_management_incident_created', + 'incident_management_incident_reopened', + 'incident_management_incident_closed', + 'incident_management_incident_assigned', + 'incident_management_incident_todo', + 'incident_management_incident_comment', + 'incident_management_incident_zoom_meeting', + 'incident_management_incident_published', + 'incident_management_incident_relate', + 'incident_management_incident_unrelate', + 'incident_management_incident_change_confidential' + ] + feature_flag: usage_data_incident_management_incidents_total_unique_counts diff --git a/lib/gitlab/usage_data_counters/base_counter.rb b/lib/gitlab/usage_data_counters/base_counter.rb index 44893645cc2..d28fd17a989 100644 --- a/lib/gitlab/usage_data_counters/base_counter.rb +++ b/lib/gitlab/usage_data_counters/base_counter.rb @@ -29,6 +29,12 @@ module Gitlab::UsageDataCounters known_events.map { |event| [counter_key(event), -1] }.to_h end + def fetch_supported_event(event_name) + return if prefix.present? && !event_name.start_with?(prefix) + + known_events.find { |event| counter_key(event) == event_name.to_sym } + end + private def require_known_event(event) diff --git a/lib/gitlab/usage_data_counters/counter_events/guest_package_events.yml b/lib/gitlab/usage_data_counters/counter_events/guest_package_events.yml new file mode 100644 index 00000000000..a9b9f8ea235 --- /dev/null +++ b/lib/gitlab/usage_data_counters/counter_events/guest_package_events.yml @@ -0,0 +1,34 @@ +--- +- i_package_composer_guest_delete +- i_package_composer_guest_pull +- i_package_composer_guest_push +- i_package_conan_guest_delete +- i_package_conan_guest_pull +- i_package_conan_guest_push +- i_package_container_guest_delete +- i_package_container_guest_pull +- i_package_container_guest_push +- i_package_debian_guest_delete +- i_package_debian_guest_pull +- i_package_debian_guest_push +- i_package_generic_guest_delete +- i_package_generic_guest_pull +- i_package_generic_guest_push +- i_package_golang_guest_delete +- i_package_golang_guest_pull +- i_package_golang_guest_push +- i_package_maven_guest_delete +- i_package_maven_guest_pull +- i_package_maven_guest_push +- i_package_npm_guest_delete +- i_package_npm_guest_pull +- i_package_npm_guest_push +- i_package_nuget_guest_delete +- i_package_nuget_guest_pull +- i_package_nuget_guest_push +- i_package_pypi_guest_delete +- i_package_pypi_guest_pull +- i_package_pypi_guest_push +- i_package_tag_guest_delete +- i_package_tag_guest_pull +- i_package_tag_guest_push diff --git a/lib/gitlab/usage_data_counters/editor_unique_counter.rb b/lib/gitlab/usage_data_counters/editor_unique_counter.rb index b68d50ee419..eeb26c11bfa 100644 --- a/lib/gitlab/usage_data_counters/editor_unique_counter.rb +++ b/lib/gitlab/usage_data_counters/editor_unique_counter.rb @@ -6,6 +6,7 @@ module Gitlab EDIT_BY_SNIPPET_EDITOR = 'g_edit_by_snippet_ide' EDIT_BY_SFE = 'g_edit_by_sfe' EDIT_BY_WEB_IDE = 'g_edit_by_web_ide' + EDIT_BY_SSE = 'g_edit_by_sse' EDIT_CATEGORY = 'ide_edit' class << self @@ -38,6 +39,14 @@ module Gitlab count_unique(events, date_from, date_to) end + def track_sse_edit_action(author:, time: Time.zone.now) + track_unique_action(EDIT_BY_SSE, author, time) + end + + def count_sse_edit_actions(date_from:, date_to:) + count_unique(EDIT_BY_SSE, date_from, date_to) + end + private def track_unique_action(action, author, time) diff --git a/lib/gitlab/usage_data_counters/guest_package_event_counter.rb b/lib/gitlab/usage_data_counters/guest_package_event_counter.rb new file mode 100644 index 00000000000..a9bcbfadda2 --- /dev/null +++ b/lib/gitlab/usage_data_counters/guest_package_event_counter.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Gitlab + module UsageDataCounters + class GuestPackageEventCounter < BaseCounter + KNOWN_EVENTS_PATH = File.expand_path('counter_events/guest_package_events.yml', __dir__) + KNOWN_EVENTS = YAML.safe_load(File.read(KNOWN_EVENTS_PATH)).freeze + PREFIX = 'package_guest' + end + end +end diff --git a/lib/gitlab/usage_data_counters/guest_package_events.yml b/lib/gitlab/usage_data_counters/guest_package_events.yml new file mode 100644 index 00000000000..a9b9f8ea235 --- /dev/null +++ b/lib/gitlab/usage_data_counters/guest_package_events.yml @@ -0,0 +1,34 @@ +--- +- i_package_composer_guest_delete +- i_package_composer_guest_pull +- i_package_composer_guest_push +- i_package_conan_guest_delete +- i_package_conan_guest_pull +- i_package_conan_guest_push +- i_package_container_guest_delete +- i_package_container_guest_pull +- i_package_container_guest_push +- i_package_debian_guest_delete +- i_package_debian_guest_pull +- i_package_debian_guest_push +- i_package_generic_guest_delete +- i_package_generic_guest_pull +- i_package_generic_guest_push +- i_package_golang_guest_delete +- i_package_golang_guest_pull +- i_package_golang_guest_push +- i_package_maven_guest_delete +- i_package_maven_guest_pull +- i_package_maven_guest_push +- i_package_npm_guest_delete +- i_package_npm_guest_pull +- i_package_npm_guest_push +- i_package_nuget_guest_delete +- i_package_nuget_guest_pull +- i_package_nuget_guest_push +- i_package_pypi_guest_delete +- i_package_pypi_guest_pull +- i_package_pypi_guest_push +- i_package_tag_guest_delete +- i_package_tag_guest_pull +- i_package_tag_guest_push diff --git a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb index da013a06777..0fed8e1c211 100644 --- a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb +++ b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb @@ -18,6 +18,7 @@ module Gitlab ISSUE_CROSS_REFERENCED = 'g_project_management_issue_cross_referenced' ISSUE_MOVED = 'g_project_management_issue_moved' ISSUE_RELATED = 'g_project_management_issue_related' + ISSUE_CLONED = 'g_project_management_issue_cloned' ISSUE_UNRELATED = 'g_project_management_issue_unrelated' ISSUE_MARKED_AS_DUPLICATE = 'g_project_management_issue_marked_as_duplicate' ISSUE_LOCKED = 'g_project_management_issue_locked' @@ -137,6 +138,10 @@ module Gitlab track_unique_action(ISSUE_COMMENT_REMOVED, author, time) end + def track_issue_cloned_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_CLONED, author, time) + end + private def track_unique_action(action, author, time) diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml index 85f16ea807b..25cf388aedf 100644 --- a/lib/gitlab/usage_data_counters/known_events/common.yml +++ b/lib/gitlab/usage_data_counters/known_events/common.yml @@ -118,6 +118,12 @@ expiry: 29 aggregation: daily feature_flag: track_editor_edit_actions +- name: g_edit_by_sse + category: ide_edit + redis_slot: edit + expiry: 29 + aggregation: daily + feature_flag: track_editor_edit_actions - name: g_edit_by_snippet_ide category: ide_edit redis_slot: edit @@ -145,6 +151,7 @@ - name: design_action category: source_code aggregation: daily + feature_flag: usage_data_design_action - name: project_action category: source_code aggregation: daily @@ -229,6 +236,12 @@ category: incident_management aggregation: weekly feature_flag: usage_data_incident_management_incident_change_confidential +# Incident management alerts +- name: incident_management_alert_create_incident + redis_slot: incident_management + category: incident_management_alerts + aggregation: weekly + feature_flag: usage_data_incident_management_alert_create_incident # Testing category - name: i_testing_test_case_parsed category: testing @@ -396,9 +409,19 @@ redis_slot: project_management aggregation: daily feature_flag: track_issue_activity_actions +- name: g_project_management_issue_cloned + category: issues_edit + redis_slot: project_management + aggregation: daily + feature_flag: track_issue_activity_actions # Secrets Management - name: i_ci_secrets_management_vault_build_created category: ci_secrets_management redis_slot: ci_secrets_management aggregation: weekly feature_flag: usage_data_i_ci_secrets_management_vault_build_created +- name: i_snippets_show + category: snippets + redis_slot: snippets + aggregation: weekly + feature_flag: usage_data_i_snippets_show diff --git a/lib/gitlab/usage_data_counters/known_events/package_events.yml b/lib/gitlab/usage_data_counters/known_events/package_events.yml index 7ed02aa2a85..4c3138dc000 100644 --- a/lib/gitlab/usage_data_counters/known_events/package_events.yml +++ b/lib/gitlab/usage_data_counters/known_events/package_events.yml @@ -1,265 +1,331 @@ --- -- name: i_package_maven_user_push - category: maven_packages - aggregation: weekly - redis_slot: package -- name: i_package_maven_deploy_token_push - category: maven_packages - aggregation: weekly - redis_slot: package -- name: i_package_maven_user_delete - category: maven_packages - aggregation: weekly - redis_slot: package -- name: i_package_maven_deploy_token_delete - category: maven_packages - aggregation: weekly - redis_slot: package -- name: i_package_maven_user_pull - category: maven_packages - aggregation: weekly - redis_slot: package -- name: i_package_maven_deploy_token_pull - category: maven_packages +- name: i_package_composer_deploy_token_delete + category: composer_packages aggregation: weekly redis_slot: package -- name: i_package_npm_user_push - category: npm_packages + feature_flag: collect_package_events_redis +- name: i_package_composer_deploy_token_pull + category: composer_packages aggregation: weekly redis_slot: package -- name: i_package_npm_deploy_token_push - category: npm_packages + feature_flag: collect_package_events_redis +- name: i_package_composer_deploy_token_push + category: composer_packages aggregation: weekly redis_slot: package -- name: i_package_npm_user_delete - category: npm_packages + feature_flag: collect_package_events_redis +- name: i_package_composer_user_delete + category: composer_packages aggregation: weekly redis_slot: package -- name: i_package_npm_deploy_token_delete - category: npm_packages + feature_flag: collect_package_events_redis +- name: i_package_composer_user_pull + category: composer_packages aggregation: weekly redis_slot: package -- name: i_package_npm_user_pull - category: npm_packages + feature_flag: collect_package_events_redis +- name: i_package_composer_user_push + category: composer_packages aggregation: weekly redis_slot: package -- name: i_package_npm_deploy_token_pull - category: npm_packages + feature_flag: collect_package_events_redis +- name: i_package_conan_deploy_token_delete + category: conan_packages aggregation: weekly redis_slot: package -- name: i_package_conan_user_push + feature_flag: collect_package_events_redis +- name: i_package_conan_deploy_token_pull category: conan_packages aggregation: weekly redis_slot: package + feature_flag: collect_package_events_redis - name: i_package_conan_deploy_token_push category: conan_packages aggregation: weekly redis_slot: package + feature_flag: collect_package_events_redis - name: i_package_conan_user_delete category: conan_packages aggregation: weekly redis_slot: package -- name: i_package_conan_deploy_token_delete - category: conan_packages - aggregation: weekly - redis_slot: package + feature_flag: collect_package_events_redis - name: i_package_conan_user_pull category: conan_packages aggregation: weekly redis_slot: package -- name: i_package_conan_deploy_token_pull + feature_flag: collect_package_events_redis +- name: i_package_conan_user_push category: conan_packages aggregation: weekly redis_slot: package -- name: i_package_nuget_user_push - category: nuget_packages - aggregation: weekly - redis_slot: package -- name: i_package_nuget_deploy_token_push - category: nuget_packages - aggregation: weekly - redis_slot: package -- name: i_package_nuget_user_delete - category: nuget_packages - aggregation: weekly - redis_slot: package -- name: i_package_nuget_deploy_token_delete - category: nuget_packages - aggregation: weekly - redis_slot: package -- name: i_package_nuget_user_pull - category: nuget_packages - aggregation: weekly - redis_slot: package -- name: i_package_nuget_deploy_token_pull - category: nuget_packages + feature_flag: collect_package_events_redis +- name: i_package_container_deploy_token_delete + category: container_packages aggregation: weekly redis_slot: package -- name: i_package_pypi_user_push - category: pypi_packages + feature_flag: collect_package_events_redis +- name: i_package_container_deploy_token_pull + category: container_packages aggregation: weekly redis_slot: package -- name: i_package_pypi_deploy_token_push - category: pypi_packages + feature_flag: collect_package_events_redis +- name: i_package_container_deploy_token_push + category: container_packages aggregation: weekly redis_slot: package -- name: i_package_pypi_user_delete - category: pypi_packages + feature_flag: collect_package_events_redis +- name: i_package_container_user_delete + category: container_packages aggregation: weekly redis_slot: package -- name: i_package_pypi_deploy_token_delete - category: pypi_packages + feature_flag: collect_package_events_redis +- name: i_package_container_user_pull + category: container_packages aggregation: weekly redis_slot: package -- name: i_package_pypi_user_pull - category: pypi_packages + feature_flag: collect_package_events_redis +- name: i_package_container_user_push + category: container_packages aggregation: weekly redis_slot: package -- name: i_package_pypi_deploy_token_pull - category: pypi_packages + feature_flag: collect_package_events_redis +- name: i_package_debian_deploy_token_delete + category: debian_packages aggregation: weekly redis_slot: package -- name: i_package_composer_user_push - category: composer_packages + feature_flag: collect_package_events_redis +- name: i_package_debian_deploy_token_pull + category: debian_packages aggregation: weekly redis_slot: package -- name: i_package_composer_deploy_token_push - category: composer_packages + feature_flag: collect_package_events_redis +- name: i_package_debian_deploy_token_push + category: debian_packages aggregation: weekly redis_slot: package -- name: i_package_composer_user_delete - category: composer_packages + feature_flag: collect_package_events_redis +- name: i_package_debian_user_delete + category: debian_packages aggregation: weekly redis_slot: package -- name: i_package_composer_deploy_token_delete - category: composer_packages + feature_flag: collect_package_events_redis +- name: i_package_debian_user_pull + category: debian_packages aggregation: weekly redis_slot: package -- name: i_package_composer_user_pull - category: composer_packages + feature_flag: collect_package_events_redis +- name: i_package_debian_user_push + category: debian_packages aggregation: weekly redis_slot: package -- name: i_package_composer_deploy_token_pull - category: composer_packages + feature_flag: collect_package_events_redis +- name: i_package_generic_deploy_token_delete + category: generic_packages aggregation: weekly redis_slot: package -- name: i_package_generic_user_push + feature_flag: collect_package_events_redis +- name: i_package_generic_deploy_token_pull category: generic_packages aggregation: weekly redis_slot: package + feature_flag: collect_package_events_redis - name: i_package_generic_deploy_token_push category: generic_packages aggregation: weekly redis_slot: package + feature_flag: collect_package_events_redis - name: i_package_generic_user_delete category: generic_packages aggregation: weekly redis_slot: package -- name: i_package_generic_deploy_token_delete + feature_flag: collect_package_events_redis +- name: i_package_generic_user_pull category: generic_packages aggregation: weekly redis_slot: package -- name: i_package_generic_user_pull + feature_flag: collect_package_events_redis +- name: i_package_generic_user_push category: generic_packages aggregation: weekly redis_slot: package -- name: i_package_generic_deploy_token_pull - category: generic_packages + feature_flag: collect_package_events_redis +- name: i_package_golang_deploy_token_delete + category: golang_packages aggregation: weekly redis_slot: package -- name: i_package_golang_user_push + feature_flag: collect_package_events_redis +- name: i_package_golang_deploy_token_pull category: golang_packages aggregation: weekly redis_slot: package + feature_flag: collect_package_events_redis - name: i_package_golang_deploy_token_push category: golang_packages aggregation: weekly redis_slot: package + feature_flag: collect_package_events_redis - name: i_package_golang_user_delete category: golang_packages aggregation: weekly redis_slot: package -- name: i_package_golang_deploy_token_delete + feature_flag: collect_package_events_redis +- name: i_package_golang_user_pull category: golang_packages aggregation: weekly redis_slot: package -- name: i_package_golang_user_pull + feature_flag: collect_package_events_redis +- name: i_package_golang_user_push category: golang_packages aggregation: weekly redis_slot: package -- name: i_package_golang_deploy_token_pull - category: golang_packages + feature_flag: collect_package_events_redis +- name: i_package_maven_deploy_token_delete + category: maven_packages aggregation: weekly redis_slot: package -- name: i_package_debian_user_push - category: debian_packages + feature_flag: collect_package_events_redis +- name: i_package_maven_deploy_token_pull + category: maven_packages aggregation: weekly redis_slot: package -- name: i_package_debian_deploy_token_push - category: debian_packages + feature_flag: collect_package_events_redis +- name: i_package_maven_deploy_token_push + category: maven_packages aggregation: weekly redis_slot: package -- name: i_package_debian_user_delete - category: debian_packages + feature_flag: collect_package_events_redis +- name: i_package_maven_user_delete + category: maven_packages aggregation: weekly redis_slot: package -- name: i_package_debian_deploy_token_delete - category: debian_packages + feature_flag: collect_package_events_redis +- name: i_package_maven_user_pull + category: maven_packages aggregation: weekly redis_slot: package -- name: i_package_debian_user_pull - category: debian_packages + feature_flag: collect_package_events_redis +- name: i_package_maven_user_push + category: maven_packages aggregation: weekly redis_slot: package -- name: i_package_debian_deploy_token_pull - category: debian_packages + feature_flag: collect_package_events_redis +- name: i_package_npm_deploy_token_delete + category: npm_packages aggregation: weekly redis_slot: package -- name: i_package_container_user_push - category: container_packages + feature_flag: collect_package_events_redis +- name: i_package_npm_deploy_token_pull + category: npm_packages aggregation: weekly redis_slot: package -- name: i_package_container_deploy_token_push - category: container_packages + feature_flag: collect_package_events_redis +- name: i_package_npm_deploy_token_push + category: npm_packages aggregation: weekly redis_slot: package -- name: i_package_container_user_delete - category: container_packages + feature_flag: collect_package_events_redis +- name: i_package_npm_user_delete + category: npm_packages aggregation: weekly redis_slot: package -- name: i_package_container_deploy_token_delete - category: container_packages + feature_flag: collect_package_events_redis +- name: i_package_npm_user_pull + category: npm_packages aggregation: weekly redis_slot: package -- name: i_package_container_user_pull - category: container_packages + feature_flag: collect_package_events_redis +- name: i_package_npm_user_push + category: npm_packages aggregation: weekly redis_slot: package -- name: i_package_container_deploy_token_pull - category: container_packages + feature_flag: collect_package_events_redis +- name: i_package_nuget_deploy_token_delete + category: nuget_packages aggregation: weekly redis_slot: package -- name: i_package_tag_user_push + feature_flag: collect_package_events_redis +- name: i_package_nuget_deploy_token_pull + category: nuget_packages + aggregation: weekly + redis_slot: package + feature_flag: collect_package_events_redis +- name: i_package_nuget_deploy_token_push + category: nuget_packages + aggregation: weekly + redis_slot: package + feature_flag: collect_package_events_redis +- name: i_package_nuget_user_delete + category: nuget_packages + aggregation: weekly + redis_slot: package + feature_flag: collect_package_events_redis +- name: i_package_nuget_user_pull + category: nuget_packages + aggregation: weekly + redis_slot: package + feature_flag: collect_package_events_redis +- name: i_package_nuget_user_push + category: nuget_packages + aggregation: weekly + redis_slot: package + feature_flag: collect_package_events_redis +- name: i_package_pypi_deploy_token_delete + category: pypi_packages + aggregation: weekly + redis_slot: package + feature_flag: collect_package_events_redis +- name: i_package_pypi_deploy_token_pull + category: pypi_packages + aggregation: weekly + redis_slot: package + feature_flag: collect_package_events_redis +- name: i_package_pypi_deploy_token_push + category: pypi_packages + aggregation: weekly + redis_slot: package + feature_flag: collect_package_events_redis +- name: i_package_pypi_user_delete + category: pypi_packages + aggregation: weekly + redis_slot: package + feature_flag: collect_package_events_redis +- name: i_package_pypi_user_pull + category: pypi_packages + aggregation: weekly + redis_slot: package + feature_flag: collect_package_events_redis +- name: i_package_pypi_user_push + category: pypi_packages + aggregation: weekly + redis_slot: package + feature_flag: collect_package_events_redis +- name: i_package_tag_deploy_token_delete category: tag_packages aggregation: weekly redis_slot: package -- name: i_package_tag_deploy_token_push + feature_flag: collect_package_events_redis +- name: i_package_tag_deploy_token_pull category: tag_packages aggregation: weekly redis_slot: package -- name: i_package_tag_user_delete + feature_flag: collect_package_events_redis +- name: i_package_tag_deploy_token_push category: tag_packages aggregation: weekly redis_slot: package -- name: i_package_tag_deploy_token_delete + feature_flag: collect_package_events_redis +- name: i_package_tag_user_delete category: tag_packages aggregation: weekly redis_slot: package + feature_flag: collect_package_events_redis - name: i_package_tag_user_pull category: tag_packages aggregation: weekly redis_slot: package -- name: i_package_tag_deploy_token_pull + feature_flag: collect_package_events_redis +- name: i_package_tag_user_push category: tag_packages aggregation: weekly redis_slot: package + feature_flag: collect_package_events_redis diff --git a/lib/gitlab/usage_data_counters/search_counter.rb b/lib/gitlab/usage_data_counters/search_counter.rb index 61f98887adc..46aec52b95a 100644 --- a/lib/gitlab/usage_data_counters/search_counter.rb +++ b/lib/gitlab/usage_data_counters/search_counter.rb @@ -4,6 +4,7 @@ module Gitlab module UsageDataCounters class SearchCounter < BaseCounter KNOWN_EVENTS = %w[all_searches navbar_searches].freeze + PREFIX = nil class << self def redis_key(event) diff --git a/lib/gitlab/usage_data_queries.rb b/lib/gitlab/usage_data_queries.rb index c54e766230e..b275bdbacde 100644 --- a/lib/gitlab/usage_data_queries.rb +++ b/lib/gitlab/usage_data_queries.rb @@ -25,6 +25,13 @@ module Gitlab relation.select(relation.all.table[column].sum).to_sql end + # For estimated distinct count use exact query instead of hll + # buckets query, because it can't be used to obtain estimations without + # supplementary ruby code present in Gitlab::Database::PostgresHll::BatchDistinctCounter + def estimate_batch_distinct_count(relation, column = nil, *rest) + raw_sql(relation, column, :distinct) + end + private def raw_sql(relation, column, distinct = nil) diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index eec89e1ab72..0af7ad6ec17 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -81,6 +81,10 @@ module Gitlab end end + def can_push_for_ref?(_) + can_do_action?(:push_code) + end + private def can_push? diff --git a/lib/gitlab/utils/usage_data.rb b/lib/gitlab/utils/usage_data.rb index 5267733d220..0d28a1cd035 100644 --- a/lib/gitlab/utils/usage_data.rb +++ b/lib/gitlab/utils/usage_data.rb @@ -38,6 +38,7 @@ module Gitlab extend self FALLBACK = -1 + DISTRIBUTED_HLL_FALLBACK = -2 def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil) if batch @@ -59,6 +60,17 @@ module Gitlab FALLBACK end + def estimate_batch_distinct_count(relation, column = nil, batch_size: nil, start: nil, finish: nil) + Gitlab::Database::PostgresHll::BatchDistinctCounter.new(relation, column).estimate_distinct_count(batch_size: batch_size, start: start, finish: finish) + rescue ActiveRecord::StatementInvalid + FALLBACK + # catch all rescue should be removed as a part of feature flag rollout issue + # https://gitlab.com/gitlab-org/gitlab/-/issues/285485 + rescue StandardError => error + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error) + DISTRIBUTED_HLL_FALLBACK + end + def sum(relation, column, batch_size: nil, start: nil, finish: nil) Gitlab::Database::BatchCount.batch_sum(relation, column, batch_size: batch_size, start: start, finish: finish) rescue ActiveRecord::StatementInvalid diff --git a/lib/gitlab/uuid.rb b/lib/gitlab/uuid.rb new file mode 100644 index 00000000000..12a4efabc44 --- /dev/null +++ b/lib/gitlab/uuid.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + class UUID + NAMESPACE_IDS = { + development: "a143e9e2-41b3-47bc-9a19-081d089229f4", + test: "a143e9e2-41b3-47bc-9a19-081d089229f4", + staging: "a6930898-a1b2-4365-ab18-12aa474d9b26", + production: "58dc0f06-936c-43b3-93bb-71693f1b6570" + }.freeze + + NAMESPACE_REGEX = /(\h{8})-(\h{4})-(\h{4})-(\h{4})-(\h{4})(\h{8})/.freeze + PACK_PATTERN = "NnnnnN".freeze + + class << self + def v5(name, namespace_id: default_namespace_id) + Digest::UUID.uuid_v5(namespace_id, name) + end + + private + + def default_namespace_id + @default_namespace_id ||= begin + namespace_uuid = NAMESPACE_IDS.fetch(Rails.env.to_sym) + # Digest::UUID is broken when using a UUID as a namespace_id + # https://github.com/rails/rails/issues/37681#issue-520718028 + namespace_uuid.scan(NAMESPACE_REGEX).flatten.map { |s| s.to_i(16) }.pack(PACK_PATTERN) + end + end + end + end +end diff --git a/lib/gitlab/whats_new.rb b/lib/gitlab/whats_new.rb deleted file mode 100644 index 69ccb48c544..00000000000 --- a/lib/gitlab/whats_new.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module WhatsNew - CACHE_DURATION = 1.hour - WHATS_NEW_FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml') - - private - - def whats_new_release_items(page: 1) - Rails.cache.fetch(whats_new_items_cache_key(page), expires_in: CACHE_DURATION) do - index = page - 1 - file_path = whats_new_file_paths[index] - - next if file_path.nil? - - file = File.read(file_path) - - items = YAML.safe_load(file, permitted_classes: [Date]) - - items if items.is_a?(Array) - end - rescue => e - Gitlab::ErrorTracking.track_exception(e, page: page) - - nil - end - - def whats_new_file_paths - @whats_new_file_paths ||= Rails.cache.fetch('whats_new:file_paths', expires_in: CACHE_DURATION) do - Dir.glob(WHATS_NEW_FILES_PATH).sort.reverse - end - end - - def whats_new_items_cache_key(page) - filename = /\d*\_\d*\_\d*/.match(whats_new_file_paths&.first) - "whats_new:release_items:file-#{filename}:page-#{page}" - end - end -end diff --git a/lib/gitlab_danger.rb b/lib/gitlab_danger.rb index bbb64e0d5da..ec9dd20ccc0 100644 --- a/lib/gitlab_danger.rb +++ b/lib/gitlab_danger.rb @@ -14,6 +14,7 @@ class GitlabDanger product_analytics utility_css pajamas + pipeline ].freeze CI_ONLY_RULES ||= %w[ diff --git a/lib/microsoft_teams/notifier.rb b/lib/microsoft_teams/notifier.rb index 0b21c355a54..39005f56dcb 100644 --- a/lib/microsoft_teams/notifier.rb +++ b/lib/microsoft_teams/notifier.rb @@ -14,7 +14,7 @@ module MicrosoftTeams response = Gitlab::HTTP.post( @webhook.to_str, headers: @header, - body: body(options) + body: body(**options) ) result = true if response @@ -27,14 +27,13 @@ module MicrosoftTeams private - def body(options = {}) + def body(title: nil, summary: nil, attachments: nil, activity:) result = { 'sections' => [] } - result['title'] = options[:title] - result['summary'] = options[:summary] - result['sections'] << MicrosoftTeams::Activity.new(options[:activity]).prepare + result['title'] = title + result['summary'] = summary + result['sections'] << MicrosoftTeams::Activity.new(**activity).prepare - attachments = options[:attachments] unless attachments.blank? result['sections'] << { text: attachments } end diff --git a/lib/object_storage/config.rb b/lib/object_storage/config.rb index cc536ce9b46..f933d4e4866 100644 --- a/lib/object_storage/config.rb +++ b/lib/object_storage/config.rb @@ -93,6 +93,11 @@ module ObjectStorage private + # This returns a Hash of HTTP encryption headers to send along to S3. + # + # They can also be passed in as Fog::AWS::Storage::File attributes, since there + # are aliases defined for them: + # https://github.com/fog/fog-aws/blob/ab288f29a0974d64fd8290db41080e5578be9651/lib/fog/aws/models/storage/file.rb#L24-L25 def aws_server_side_encryption_headers { 'x-amz-server-side-encryption' => server_side_encryption, diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb index b5864382299..3a8fa51e198 100644 --- a/lib/object_storage/direct_upload.rb +++ b/lib/object_storage/direct_upload.rb @@ -184,15 +184,20 @@ module ObjectStorage private def rounded_multipart_part_size - # round multipart_part_size up to minimum_mulitpart_size + # round multipart_part_size up to minimum_multipart_size (multipart_part_size + MINIMUM_MULTIPART_SIZE - 1) / MINIMUM_MULTIPART_SIZE * MINIMUM_MULTIPART_SIZE end def multipart_part_size + return MINIMUM_MULTIPART_SIZE if maximum_size == 0 + maximum_size / number_of_multipart_parts end def number_of_multipart_parts + # If we don't have max length, we can only assume the file is as large as possible. + return MAXIMUM_MULTIPART_PARTS if maximum_size == 0 + [ # round maximum_size up to minimum_mulitpart_size (maximum_size + MINIMUM_MULTIPART_SIZE - 1) / MINIMUM_MULTIPART_SIZE, @@ -201,7 +206,7 @@ module ObjectStorage end def requires_multipart_upload? - config.aws? && !has_length + config.aws? && !has_length && !use_workhorse_s3_client? end def upload_id diff --git a/lib/product_analytics/tracker.rb b/lib/product_analytics/tracker.rb index 2dc5e1f53ce..d4a88b879f0 100644 --- a/lib/product_analytics/tracker.rb +++ b/lib/product_analytics/tracker.rb @@ -7,36 +7,5 @@ module ProductAnalytics # The collector URL minus protocol and /i COLLECTOR_URL = Gitlab.config.gitlab.url.sub(/\Ahttps?\:\/\//, '') + '/-/collector' - - class << self - include Gitlab::Utils::StrongMemoize - - def event(category, action, label: nil, property: nil, value: nil, context: nil) - return unless enabled? - - snowplow.track_struct_event(category, action, label, property, value, context, (Time.now.to_f * 1000).to_i) - end - - private - - def enabled? - Gitlab::CurrentSettings.usage_ping_enabled? - end - - def project_id - Gitlab::CurrentSettings.self_monitoring_project_id - end - - def snowplow - strong_memoize(:snowplow) do - SnowplowTracker::Tracker.new( - SnowplowTracker::AsyncEmitter.new(COLLECTOR_URL, protocol: Gitlab.config.gitlab.protocol), - SnowplowTracker::Subject.new, - Gitlab::Tracking::SNOWPLOW_NAMESPACE, - project_id.to_s - ) - end - end - end end end diff --git a/lib/quality/test_level.rb b/lib/quality/test_level.rb index b239b6812ca..45cfa9b373d 100644 --- a/lib/quality/test_level.rb +++ b/lib/quality/test_level.rb @@ -21,6 +21,9 @@ module Quality config db dependencies + elastic + elastic_integration + experiments factories finders frontend @@ -46,7 +49,6 @@ module Quality validators views workers - elastic_integration tooling ], integration: %w[ diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake index e2c92054d62..e03c78d5a40 100644 --- a/lib/tasks/gettext.rake +++ b/lib/tasks/gettext.rake @@ -1,39 +1,38 @@ +# frozen_string_literal: true + require "gettext_i18n_rails/tasks" namespace :gettext do - # Customize list of translatable files - # See: https://github.com/grosser/gettext_i18n_rails#customizing-list-of-translatable-files - def files_to_translate - folders = %W(ee app lib config #{locale_path}).join(',') - exts = %w(rb erb haml slim rhtml js jsx vue handlebars hbs mustache).join(',') - - Dir.glob( - "{#{folders}}/**/*.{#{exts}}" - ) - end - - # Disallow HTML from translatable strings - # See: https://docs.gitlab.com/ee/development/i18n/externalization.html#html - def html_todolist - return @html_todolist if defined?(@html_todolist) - - @html_todolist = YAML.load_file(Rails.root.join('lib/gitlab/i18n/html_todo.yml')) - end - task :compile do # See: https://gitlab.com/gitlab-org/gitlab-foss/issues/33014#note_31218998 - FileUtils.touch(File.join(Rails.root, 'locale/gitlab.pot')) + FileUtils.touch(pot_file_path) Rake::Task['gettext:po_to_json'].invoke end desc 'Regenerate gitlab.pot file' task :regenerate do - pot_file = 'locale/gitlab.pot' - # Remove all translated files, this speeds up finding - FileUtils.rm Dir['locale/**/gitlab.*'] + ensure_locale_folder_presence! + + # Clean up folders that do not contain a gitlab.po file + Pathname.new(locale_path).children.each do |child| + next unless child.directory? + + folder_path = child.to_path + + if File.exist?("#{folder_path}/gitlab.po") + # remove all translated files to speed up finding + FileUtils.rm Dir["#{folder_path}/gitlab.*"] + else + # remove empty translation folders so we don't generate un-needed .po files + puts "Deleting #{folder_path} as it does not contain a 'gitlab.po' file." + + FileUtils.rm_r folder_path + end + end + # remove the `pot` file to ensure it's completely regenerated - FileUtils.rm_f pot_file + FileUtils.rm_f(pot_file_path) Rake::Task['gettext:find'].invoke @@ -42,10 +41,12 @@ namespace :gettext do raise 'failed to cleanup generated locale/*/gitlab.po files' end + raise 'gitlab.pot file not generated' unless File.exist?(pot_file_path) + # Remove timestamps from the pot file - pot_content = File.read pot_file + pot_content = File.read pot_file_path pot_content.gsub!(/^"POT?\-(?:Creation|Revision)\-Date\:.*\n/, '') - File.write pot_file, pot_content + File.write pot_file_path, pot_content puts <<~MSG All done. Please commit the changes to `locale/gitlab.pot`. @@ -64,11 +65,10 @@ namespace :gettext do linters = files.map do |file| locale = File.basename(File.dirname(file)) - Gitlab::I18n::PoLinter.new(po_path: file, html_todolist: html_todolist, locale: locale) + Gitlab::I18n::PoLinter.new(po_path: file, locale: locale) end - pot_file = Rails.root.join('locale/gitlab.pot') - linters.unshift(Gitlab::I18n::PoLinter.new(po_path: pot_file, html_todolist: html_todolist)) + linters.unshift(Gitlab::I18n::PoLinter.new(po_path: pot_file_path)) failed_linters = linters.select { |linter| linter.errors.any? } @@ -84,12 +84,11 @@ namespace :gettext do end task :updated_check do - pot_file = 'locale/gitlab.pot' # Removing all pre-translated files speeds up `gettext:find` as the # files don't need to be merged. # Having `LC_MESSAGES/gitlab.mo files present also confuses the output. FileUtils.rm Dir['locale/**/gitlab.*'] - FileUtils.rm_f pot_file + FileUtils.rm_f pot_file_path # `gettext:find` writes touches to temp files to `stderr` which would cause # `static-analysis` to report failures. We can ignore these. @@ -97,18 +96,18 @@ namespace :gettext do Rake::Task['gettext:find'].invoke end - pot_diff = `git diff -- #{pot_file} | grep -E '^(\\+|-)msgid'`.strip + pot_diff = `git diff -- #{pot_file_path} | grep -E '^(\\+|-)msgid'`.strip # reset the locale folder for potential next tasks `git checkout -- locale` if pot_diff.present? raise <<~MSG - Changes in translated strings found, please update file `#{pot_file}` by running: + Changes in translated strings found, please update file `#{pot_file_path}` by running: bin/rake gettext:regenerate - Then commit and push the resulting changes to `#{pot_file}`. + Then commit and push the resulting changes to `#{pot_file_path}`. The diff was: @@ -117,6 +116,27 @@ namespace :gettext do end end + private + + # Customize list of translatable files + # See: https://github.com/grosser/gettext_i18n_rails#customizing-list-of-translatable-files + def files_to_translate + folders = %W(ee app lib config #{locale_path}).join(',') + exts = %w(rb erb haml slim rhtml js jsx vue handlebars hbs mustache).join(',') + + Dir.glob( + "{#{folders}}/**/*.{#{exts}}" + ) + end + + # Disallow HTML from translatable strings + # See: https://docs.gitlab.com/ee/development/i18n/externalization.html#html + def html_todolist + return @html_todolist if defined?(@html_todolist) + + @html_todolist = YAML.safe_load(File.read(Rails.root.join('lib/gitlab/i18n/html_todo.yml'))) + end + def report_errors_for_file(file, errors_for_file) puts "Errors in `#{file}`:" @@ -140,4 +160,21 @@ namespace :gettext do $stderr.reopen(old_stderr) old_stderr.close end + + def ensure_locale_folder_presence! + unless Dir.exist?(locale_path) + raise <<~MSG + Cannot find '#{locale_path}' folder. Please ensure you're running this task from the gitlab repo. + + MSG + end + end + + def locale_path + @locale_path ||= Rails.root.join('locale') + end + + def pot_file_path + @pot_file_path ||= File.join(locale_path, 'gitlab.pot') + end end diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index ab2d77eeaf0..54e74fd9c8b 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -81,7 +81,10 @@ namespace :gitlab do if head_assets_md5 != master_assets_md5 || !public_assets_webpack_dir_exists FileUtils.rm_r(Tasks::Gitlab::Assets::PUBLIC_ASSETS_WEBPACK_DIR) if public_assets_webpack_dir_exists - system('yarn webpack') + + unless system('yarn webpack') + abort 'Error: Unable to compile webpack production bundle.'.color(:red) + end end end diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index a3f20f31f64..901e349ea31 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -192,16 +192,42 @@ namespace :gitlab do exit end - indexes = if args[:index_name] - [Gitlab::Database::PostgresIndex.by_identifier(args[:index_name])] - else - Gitlab::Database::Reindexing.candidate_indexes.random_few(2) - end + indexes = Gitlab::Database::Reindexing.candidate_indexes + + if identifier = args[:index_name] + raise ArgumentError, "Index name is not fully qualified with a schema: #{identifier}" unless identifier =~ /^\w+\.\w+$/ + + indexes = indexes.where(identifier: identifier) + + raise "Index not found or not supported: #{args[:index_name]}" if indexes.empty? + end + + ActiveRecord::Base.logger = Logger.new(STDOUT) if Gitlab::Utils.to_boolean(ENV['LOG_QUERIES_TO_CONSOLE'], default: false) Gitlab::Database::Reindexing.perform(indexes) rescue => e Gitlab::AppLogger.error(e) raise end + + desc 'Check if there have been user additions to the database' + task active: :environment do + if ActiveRecord::Base.connection.migration_context.needs_migration? + puts "Migrations pending. Database not active" + exit 1 + end + + # A list of projects that GitLab creates automatically on install/upgrade + # gc = Gitlab::CurrentSettings.current_application_settings + seed_projects = [Gitlab::CurrentSettings.current_application_settings.self_monitoring_project] + + if (Project.count - seed_projects.count {|x| !x.nil? }).eql?(0) + puts "No user created projects. Database not active" + exit 1 + end + + puts "Found user created projects. Database active" + exit 0 + end end end diff --git a/lib/tasks/gitlab/ldap.rake b/lib/tasks/gitlab/ldap.rake index 0459de27c96..fe7920c621f 100644 --- a/lib/tasks/gitlab/ldap.rake +++ b/lib/tasks/gitlab/ldap.rake @@ -36,5 +36,23 @@ namespace :gitlab do puts "Successfully updated #{plural_updated_count} out of #{plural_id_count} total" end end + + namespace :secret do + desc 'GitLab | LDAP | Secret | Write LDAP secrets' + task write: [:environment] do + content = STDIN.tty? ? STDIN.gets : STDIN.read + Gitlab::EncryptedLdapCommand.write(content) + end + + desc 'GitLab | LDAP | Secret | Edit LDAP secrets' + task edit: [:environment] do + Gitlab::EncryptedLdapCommand.edit + end + + desc 'GitLab | LDAP | Secret | Show LDAP secrets' + task show: [:environment] do + Gitlab::EncryptedLdapCommand.show + end + end end end diff --git a/lib/tasks/gitlab/packages/events.rake b/lib/tasks/gitlab/packages/events.rake index 3484b9b6072..ca507fb5320 100644 --- a/lib/tasks/gitlab/packages/events.rake +++ b/lib/tasks/gitlab/packages/events.rake @@ -5,11 +5,29 @@ namespace :gitlab do namespace :packages do namespace :events do task generate: :environment do + Rake::Task["gitlab:packages:events:generate_guest"].invoke + Rake::Task["gitlab:packages:events:generate_unique"].invoke + rescue => e + logger.error("Error building events list: #{e}") + end + + task generate_guest: :environment do logger = Logger.new(STDOUT) logger.info('Building list of package events...') - path = File.join(File.dirname(::Gitlab::UsageDataCounters::HLLRedisCounter::KNOWN_EVENTS_PATH), 'package_events.yml') + path = Gitlab::UsageDataCounters::GuestPackageEventCounter::KNOWN_EVENTS_PATH + File.open(path, "w") { |file| file << guest_events_list.to_yaml } + + logger.info("Events file `#{path}` generated successfully") + rescue => e + logger.error("Error building events list: #{e}") + end + + task generate_unique: :environment do + logger = Logger.new(STDOUT) + logger.info('Building list of package events...') + path = File.join(File.dirname(Gitlab::UsageDataCounters::HLLRedisCounter::KNOWN_EVENTS_PATH), 'package_events.yml') File.open(path, "w") { |file| file << generate_unique_events_list.to_yaml } logger.info("Events file `#{path}` generated successfully") @@ -17,23 +35,34 @@ namespace :gitlab do logger.error("Error building events list: #{e}") end + private + def event_pairs - ::Packages::Event.event_types.keys.product(::Packages::Event.originator_types.keys) + Packages::Event.event_types.keys.product(Packages::Event::EVENT_SCOPES.keys) end def generate_unique_events_list - ::Packages::Event::EVENT_SCOPES.keys.each_with_object([]) do |event_scope, events| - event_pairs.each do |event_type, originator| - if name = ::Packages::Event.allowed_event_name(event_scope, event_type, originator) + events = event_pairs.each_with_object([]) do |(event_type, event_scope), events| + Packages::Event.originator_types.keys.excluding('guest').each do |originator| + if name = Packages::Event.allowed_event_name(event_scope, event_type, originator) events << { "name" => name, "category" => "#{event_scope}_packages", "aggregation" => "weekly", - "redis_slot" => "package" + "redis_slot" => "package", + "feature_flag" => "collect_package_events_redis" } end end end + + events.sort_by { |event| event["name"] } + end + + def guest_events_list + event_pairs.map do |event_type, event_scope| + Packages::Event.allowed_event_name(event_scope, event_type, "guest") + end.compact.sort end end end diff --git a/lib/tasks/gitlab/usage_data.rake b/lib/tasks/gitlab/usage_data.rake index 6f3db91c2b0..d6f5661d5eb 100644 --- a/lib/tasks/gitlab/usage_data.rake +++ b/lib/tasks/gitlab/usage_data.rake @@ -9,5 +9,17 @@ namespace :gitlab do task dump_sql_in_json: :environment do puts Gitlab::Json.pretty_generate(Gitlab::UsageDataQueries.uncached_data) end + + desc 'GitLab | UsageData | Generate usage ping in JSON' + task generate: :environment do + puts Gitlab::Json.pretty_generate(Gitlab::UsageData.uncached_data) + end + + desc 'GitLab | UsageData | Generate usage ping and send it to Versions Application' + task generate_and_send: :environment do + result = SubmitUsagePingService.new.execute + + puts Gitlab::Json.pretty_generate(result.attributes) + end end end diff --git a/lib/tasks/gitlab/user_management.rake b/lib/tasks/gitlab/user_management.rake new file mode 100644 index 00000000000..f47e549e795 --- /dev/null +++ b/lib/tasks/gitlab/user_management.rake @@ -0,0 +1,13 @@ +namespace :gitlab do + namespace :user_management do + desc "GitLab | User management | Update all users of a group with personal project limit to 0 and can_create_group to false" + task :disable_project_and_group_creation, [:group_id] => :environment do |t, args| + group = Group.find(args.group_id) + + result = User.where(id: group.direct_and_indirect_users_with_inactive.select(:id)).update_all(projects_limit: 0, can_create_group: false) + ids_count = group.direct_and_indirect_users_with_inactive.count + puts "Done".color(:green) if result == ids_count + puts "Something went wrong".color(:red) if result != ids_count + end + end +end diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake index 15084a118b7..2d72a01f66f 100644 --- a/lib/tasks/gitlab/workhorse.rake +++ b/lib/tasks/gitlab/workhorse.rake @@ -8,18 +8,25 @@ namespace :gitlab do abort %(Please specify the directory where you want to install gitlab-workhorse:\n rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]") end + # It used to be the case that the binaries in the target directory match + # the source code. An administrator could run `make` to rebuild the + # binaries for instance. Or they could read the source code, or run `git + # log` to see what changed. Or they could patch workhorse for some + # reason and recompile it. None of those things make sense anymore once + # the transition in https://gitlab.com/groups/gitlab-org/-/epics/4826 is + # done: there would be an outdated copy of the workhorse source code for + # the administrator to poke at. + # + # To prevent this possible confusion and make clear what is going on, we + # have created a special branch `workhorse-move-notice` in the old + # gitlab-workhorse repository which contains no Go files anymore, just a + # README explaining what is going on. See: + # https://gitlab.com/gitlab-org/gitlab-workhorse/tree/workhorse-move-notice + # args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitlab-workhorse.git') + checkout_or_clone_version(version: 'workhorse-move-notice', repo: args.repo, target_dir: args.dir, clone_opts: %w[--depth 1]) - version = Gitlab::Workhorse.version - - checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir, clone_opts: %w[--depth 1]) - - _, status = Gitlab::Popen.popen(%w[which gmake]) - command = status == 0 ? 'gmake' : 'make' - - Dir.chdir(args.dir) do - run_command!([command]) - end + Gitlab::SetupHelper::Workhorse.compile_into(args.dir) end end end |