diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
commit | 85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch) | |
tree | 9160f299afd8c80c038f08e1545be119f5e3f1e1 /lib | |
parent | 15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff) | |
download | gitlab-ce-85dc423f7090da0a52c73eb66faf22ddb20efff9.tar.gz |
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'lib')
356 files changed, 7207 insertions, 1715 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb index 2be6792af5f..b37751e1b47 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -167,6 +167,7 @@ module API mount ::API::GroupVariables mount ::API::ImportBitbucketServer mount ::API::ImportGithub + mount ::API::IssueLinks mount ::API::Issues mount ::API::JobArtifacts mount ::API::Jobs @@ -193,9 +194,11 @@ module API mount ::API::NugetPackages mount ::API::PypiPackages mount ::API::ComposerPackages - mount ::API::ConanPackages + mount ::API::ConanProjectPackages + mount ::API::ConanInstancePackages mount ::API::MavenPackages mount ::API::NpmPackages + mount ::API::GenericPackages mount ::API::GoProxy mount ::API::Pages mount ::API::PagesDomains @@ -233,6 +236,7 @@ module API mount ::API::Templates mount ::API::Todos mount ::API::Triggers + mount ::API::UsageData mount ::API::UserCounts mount ::API::Users mount ::API::Variables @@ -244,6 +248,16 @@ module API mount ::API::Internal::Pages mount ::API::Internal::Kubernetes + version 'v3', using: :path do + # Although the following endpoints are kept behind V3 namespace, + # they're not deprecated neither should be removed when V3 get + # removed. They're needed as a layer to integrate with Jira + # Development Panel. + namespace '/', requirements: ::API::V3::Github::ENDPOINT_REQUIREMENTS do + mount ::API::V3::Github + end + end + route :any, '*path' do error!('404 Not Found', 404) end diff --git a/lib/api/applications.rb b/lib/api/applications.rb index 4e8d68c8d09..4f2c3ee79ef 100644 --- a/lib/api/applications.rb +++ b/lib/api/applications.rb @@ -6,6 +6,15 @@ module API before { authenticated_as_admin! } resource :applications do + helpers do + def validate_redirect_uri(value) + uri = ::URI.parse(value) + !uri.is_a?(URI::HTTP) || uri.host + rescue URI::InvalidURIError + false + end + end + desc 'Create a new application' do detail 'This feature was introduced in GitLab 10.5' success Entities::ApplicationWithSecret @@ -19,6 +28,13 @@ module API desc: 'Application will be used where the client secret is confidential' end post do + # Validate that host in uri is specified + # Please remove it when https://github.com/doorkeeper-gem/doorkeeper/pull/1440 is merged + # and the doorkeeper gem version is bumped + unless validate_redirect_uri(declared_params[:redirect_uri]) + render_api_error!({ redirect_uri: ["must be an absolute URI."] }, :bad_request) + end + application = Doorkeeper::Application.new(declared_params) if application.save diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index a010e0dd761..045f81074a7 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -15,6 +15,24 @@ module API detail 'This feature was introduced in GitLab 8.11.' success Entities::Ci::PipelineBasic end + + helpers do + params :optional_scope do + optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', + values: ::CommitStatus::AVAILABLE_STATUSES, + coerce_with: ->(scope) { + case scope + when String + [scope] + when ::Array + scope + else + ['unknown'] + end + } + end + end + params do use :pagination optional :scope, type: String, values: %w[running pending finished branches tags], @@ -96,6 +114,64 @@ module API present pipeline, with: Entities::Ci::Pipeline end + desc 'Get pipeline jobs' do + success Entities::Ci::Job + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + use :optional_scope + use :pagination + end + + get ':id/pipelines/:pipeline_id/jobs' do + authorize!(:read_pipeline, user_project) + + pipeline = user_project.all_pipelines.find(params[:pipeline_id]) + + if Feature.enabled?(:ci_jobs_finder_refactor) + builds = ::Ci::JobsFinder + .new(current_user: current_user, pipeline: pipeline, params: params) + .execute + else + authorize!(:read_build, pipeline) + builds = pipeline.builds + builds = filter_builds(builds, params[:scope]) + end + + builds = builds.with_preloads + + present paginate(builds), with: Entities::Ci::Job + end + + desc 'Get pipeline bridge jobs' do + success Entities::Ci::Bridge + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + use :optional_scope + use :pagination + end + + get ':id/pipelines/:pipeline_id/bridges' do + authorize!(:read_build, user_project) + + pipeline = user_project.all_pipelines.find(params[:pipeline_id]) + + if Feature.enabled?(:ci_jobs_finder_refactor) + bridges = ::Ci::JobsFinder + .new(current_user: current_user, pipeline: pipeline, params: params, type: ::Ci::Bridge) + .execute + else + authorize!(:read_pipeline, pipeline) + bridges = pipeline.bridges + bridges = filter_builds(bridges, params[:scope]) + end + + bridges = bridges.with_preloads + + present paginate(bridges), with: Entities::Ci::Bridge + end + desc 'Gets the variables for a given pipeline' do detail 'This feature was introduced in GitLab 11.11' success Entities::Ci::Variable @@ -170,6 +246,21 @@ module API end helpers do + # NOTE: This method should be removed once the ci_jobs_finder_refactor FF is + # removed. https://gitlab.com/gitlab-org/gitlab/-/issues/245183 + # rubocop: disable CodeReuse/ActiveRecord + def filter_builds(builds, scope) + return builds if scope.nil? || scope.empty? + + available_statuses = ::CommitStatus::AVAILABLE_STATUSES + + unknown = scope - available_statuses + render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty? + + builds.where(status: scope) + end + # rubocop: enable CodeReuse/ActiveRecord + def pipeline strong_memoize(:pipeline) do user_project.all_pipelines.find(params[:pipeline_id]) @@ -178,7 +269,7 @@ module API def latest_pipeline strong_memoize(:latest_pipeline) do - user_project.latest_pipeline_for_ref(params[:ref]) + user_project.latest_pipeline(params[:ref]) end end end diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 31be1bb7e3e..08903dce3dc 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -159,29 +159,29 @@ module API end desc 'Updates a job' do - http_codes [[200, 'Job was updated'], [403, 'Forbidden']] + http_codes [[200, 'Job was updated'], + [202, 'Update accepted'], + [400, 'Unknown parameters'], + [403, 'Forbidden']] end params do requires :token, type: String, desc: %q(Runners's authentication token) requires :id, type: Integer, desc: %q(Job's ID) optional :trace, type: String, desc: %q(Job's full trace) 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) end put '/:id' do job = authenticate_job! - job.trace.set(params[:trace]) if params[:trace] - Gitlab::Metrics.add_event(:update_build) - case params[:state].to_s - when 'running' - job.touch if job.needs_touch? - when 'success' - job.success! - when 'failed' - job.drop!(params[:failure_reason] || :unknown_failure) + service = ::Ci::UpdateBuildStateService + .new(job, declared_params(include_missing: false)) + + service.execute.then do |result| + status result.status end end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 140351c9e5c..9f5a6e87505 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -117,8 +117,10 @@ module API render_api_error!('invalid state', 400) end - MergeRequest.where(source_project: user_project, source_branch: ref) - .update_all(head_pipeline_id: pipeline.id) if pipeline.latest? + if pipeline.latest? + MergeRequest.where(source_project: user_project, source_branch: ref) + .update_all(head_pipeline_id: pipeline.id) + end present status, with: Entities::CommitStatus rescue StateMachines::InvalidTransition => e diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 3c7ed2a25a0..20877fb5c5f 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -136,7 +136,10 @@ module API if result[:status] == :success commit_detail = user_project.repository.commit(result[:result]) - Gitlab::UsageDataCounters::WebIdeCounter.increment_commits_count if find_user_from_warden + if find_user_from_warden + Gitlab::UsageDataCounters::WebIdeCounter.increment_commits_count + Gitlab::UsageDataCounters::EditorUniqueCounter.track_web_ide_edit_action(author: current_user) + end present commit_detail, with: Entities::CommitDetail, stats: params[:stats] else diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb index 05887e58425..31d097c4bea 100644 --- a/lib/api/composer_packages.rb +++ b/lib/api/composer_packages.rb @@ -123,7 +123,7 @@ module API bad_request! end - track_event('push_package') + package_event('push_package') ::Packages::Composer::CreatePackageService .new(authorized_user_project, current_user, declared_params) diff --git a/lib/api/conan_instance_packages.rb b/lib/api/conan_instance_packages.rb new file mode 100644 index 00000000000..209748d79fa --- /dev/null +++ b/lib/api/conan_instance_packages.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Conan Instance-Level Package Manager Client API +module API + class ConanInstancePackages < Grape::API::Instance + namespace 'packages/conan/v1' do + include ConanPackageEndpoints + end + end +end diff --git a/lib/api/conan_packages.rb b/lib/api/conan_package_endpoints.rb index 7f2afea9931..445447cfcd2 100644 --- a/lib/api/conan_packages.rb +++ b/lib/api/conan_package_endpoints.rb @@ -9,8 +9,8 @@ # # Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798 module API - class ConanPackages < Grape::API::Instance - helpers ::API::Helpers::PackagesManagerClientsHelpers + module ConanPackageEndpoints + extend ActiveSupport::Concern PACKAGE_REQUIREMENTS = { package_name: API::NO_SLASH_URL_PART_REGEX, @@ -28,15 +28,19 @@ module API CONAN_FILES = (Gitlab::Regex::Packages::CONAN_RECIPE_FILES + Gitlab::Regex::Packages::CONAN_PACKAGE_FILES).freeze - before do - require_packages_enabled! + included do + helpers ::API::Helpers::PackagesManagerClientsHelpers + helpers ::API::Helpers::Packages::Conan::ApiHelpers + helpers ::API::Helpers::RelatedResourcesHelpers - # 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 + 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 - namespace 'packages/conan/v1' do desc 'Ping the Conan API' do detail 'This feature was introduced in GitLab 12.2' end @@ -115,7 +119,7 @@ module API authorize!(:read_package, project) presenter = ::Packages::Conan::PackagePresenter.new( - recipe, + package, current_user, project, conan_package_reference: params[:conan_package_reference] @@ -133,7 +137,7 @@ module API get do authorize!(:read_package, project) - presenter = ::Packages::Conan::PackagePresenter.new(recipe, current_user, project) + presenter = ::Packages::Conan::PackagePresenter.new(package, current_user, project) present presenter, with: ::API::Entities::ConanPackage::ConanRecipeSnapshot end @@ -242,7 +246,7 @@ module API delete do authorize!(:destroy_package, project) - track_event('delete_package') + package_event('delete_package', category: 'API::ConanPackages') package.destroy end @@ -295,7 +299,7 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true put 'authorize' do - authorize_workhorse!(subject: project) + authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size) end end @@ -322,7 +326,7 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true put 'authorize' do - authorize_workhorse!(subject: project) + authorize_workhorse!(subject: project, maximum_size: project.actual_limits.conan_max_file_size) end desc 'Upload package files' do @@ -341,11 +345,5 @@ module API end end end - - helpers do - include Gitlab::Utils::StrongMemoize - include ::API::Helpers::RelatedResourcesHelpers - include ::API::Helpers::Packages::Conan::ApiHelpers - end end end diff --git a/lib/api/conan_project_packages.rb b/lib/api/conan_project_packages.rb new file mode 100644 index 00000000000..c51992231a7 --- /dev/null +++ b/lib/api/conan_project_packages.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Conan Project-Level Package Manager Client API +module API + class ConanProjectPackages < Grape::API::Instance + params do + requires :id, type: Integer, desc: 'The ID of a project', regexp: %r{\A[1-9]\d*\z} + end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + namespace ':id/packages/conan/v1' do + include ConanPackageEndpoints + end + end + end +end diff --git a/lib/api/entities/issue_link.rb b/lib/api/entities/issue_link.rb new file mode 100644 index 00000000000..8e24b046325 --- /dev/null +++ b/lib/api/entities/issue_link.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class IssueLink < Grape::Entity + expose :source, as: :source_issue, using: ::API::Entities::IssueBasic + expose :target, as: :target_issue, using: ::API::Entities::IssueBasic + expose :link_type + end + end +end diff --git a/lib/api/entities/milestone.rb b/lib/api/entities/milestone.rb index 5a0c222d691..b191210a234 100644 --- a/lib/api/entities/milestone.rb +++ b/lib/api/entities/milestone.rb @@ -10,6 +10,7 @@ module API expose :state, :created_at, :updated_at expose :due_date expose :start_date + expose :expired?, as: :expired expose :web_url do |milestone, _options| Gitlab::UrlBuilder.build(milestone) diff --git a/lib/api/entities/package.rb b/lib/api/entities/package.rb index 670965b225c..d903f50befa 100644 --- a/lib/api/entities/package.rb +++ b/lib/api/entities/package.rb @@ -28,7 +28,7 @@ module API expose :pipeline, if: ->(package) { package.build_info }, using: Package::Pipeline - expose :versions, using: ::API::Entities::PackageVersion + expose :versions, using: ::API::Entities::PackageVersion, unless: ->(_, opts) { opts[:collection] } private diff --git a/lib/api/entities/related_issue.rb b/lib/api/entities/related_issue.rb new file mode 100644 index 00000000000..491c606bd49 --- /dev/null +++ b/lib/api/entities/related_issue.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class RelatedIssue < ::API::Entities::Issue + expose :issue_link_id + expose :issue_link_type, as: :link_type + end + end +end diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb new file mode 100644 index 00000000000..98b8a40c7c9 --- /dev/null +++ b/lib/api/generic_packages.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module API + class GenericPackages < Grape::API::Instance + before do + require_packages_enabled! + authenticate! + + require_generic_packages_available! + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + route_setting :authentication, job_token_allowed: true + + namespace ':id/packages/generic' do + get 'ping' do + :pong + end + end + end + + helpers do + include ::API::Helpers::PackagesHelpers + + def require_generic_packages_available! + not_found! unless Feature.enabled?(:generic_packages, user_project) + end + end + end +end diff --git a/lib/api/github/entities.rb b/lib/api/github/entities.rb new file mode 100644 index 00000000000..c28a0b8eb7e --- /dev/null +++ b/lib/api/github/entities.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +# Simplified version of Github API entities. +# It's mainly used to mimic Github API and integrate with Jira Development Panel. +# +module API + module Github + module Entities + class Repository < Grape::Entity + expose :id + expose :owner do |project, options| + root_namespace = options[:root_namespace] || project.root_namespace + + { login: root_namespace.path } + end + expose :name do |project, options| + ::Gitlab::Jira::Dvcs.encode_project_name(project) + end + end + + class BranchCommit < Grape::Entity + expose :id, as: :sha + expose :type do |_| + 'commit' + end + end + + class RepoCommit < Grape::Entity + expose :id, as: :sha + expose :author do |commit| + { + login: commit.author&.username, + email: commit.author_email + } + end + expose :committer do |commit| + { + login: commit.author&.username, + email: commit.committer_email + } + end + expose :commit do |commit| + { + author: { + name: commit.author_name, + email: commit.author_email, + date: commit.authored_date.iso8601, + type: 'User' + }, + committer: { + name: commit.committer_name, + email: commit.committer_email, + date: commit.committed_date.iso8601, + type: 'User' + }, + message: commit.safe_message + } + end + expose :parents do |commit| + commit.parent_ids.map { |id| { sha: id } } + end + expose :files do |commit| + commit.diffs.diff_files.flat_map do |diff| + additions = diff.added_lines + deletions = diff.removed_lines + + if diff.new_file? + { + status: 'added', + filename: diff.new_path, + additions: additions, + changes: additions + } + elsif diff.deleted_file? + { + status: 'removed', + filename: diff.old_path, + deletions: deletions, + changes: deletions + } + elsif diff.renamed_file? + [ + { + status: 'removed', + filename: diff.old_path, + deletions: deletions, + changes: deletions + }, + { + status: 'added', + filename: diff.new_path, + additions: additions, + changes: additions + } + ] + else + { + status: 'modified', + filename: diff.new_path, + additions: additions, + deletions: deletions, + changes: (additions + deletions) + } + end + end + end + end + + class Branch < Grape::Entity + expose :name + + expose :commit, using: BranchCommit do |repo_branch, options| + options[:project].repository.commit(repo_branch.dereferenced_target) + end + end + + class User < Grape::Entity + expose :id + expose :username, as: :login + expose :user_url, as: :url + expose :user_url, as: :html_url + expose :avatar_url + + private + + def user_url + Gitlab::Routing.url_helpers.user_url(object) + end + end + + class NoteableComment < Grape::Entity + expose :id + expose :author, as: :user, using: User + expose :note, as: :body + expose :created_at + end + + class PullRequest < Grape::Entity + expose :title + expose :assignee, using: User do |merge_request| + merge_request.assignee + end + expose :author, as: :user, using: User + expose :created_at + expose :description, as: :body + # Since Jira service requests `/repos/-/jira/pulls` (without project + # scope), we need to make it work with ID instead IID. + expose :id, as: :number + # GitHub doesn't have a "merged" or "closed" state. It's just "open" or + # "closed". + expose :state do |merge_request| + case merge_request.state + when 'opened', 'locked' + 'open' + when 'merged' + 'closed' + else + merge_request.state + end + end + expose :merged?, as: :merged + expose :merged_at do |merge_request| + merge_request.metrics&.merged_at + end + expose :closed_at do |merge_request| + merge_request.metrics&.latest_closed_at + end + expose :updated_at + expose :html_url do |merge_request| + Gitlab::UrlBuilder.build(merge_request) + end + expose :head do + expose :source_branch, as: :label + expose :source_branch, as: :ref + expose :source_project, as: :repo, using: Repository + end + expose :base do + expose :target_branch, as: :label + expose :target_branch, as: :ref + expose :target_project, as: :repo, using: Repository + end + end + + class PullRequestPayload < Grape::Entity + expose :action do |merge_request| + case merge_request.state + when 'merged', 'closed' + 'closed' + else + 'opened' + end + end + + expose :id + expose :pull_request, using: PullRequest do |merge_request| + merge_request + end + end + + class PullRequestEvent < Grape::Entity + expose :id do |merge_request| + updated_at = merge_request.updated_at.to_i + "#{merge_request.id}-#{updated_at}" + end + expose :type do |_merge_request| + 'PullRequestEvent' + end + expose :updated_at, as: :created_at + expose :payload, using: PullRequestPayload do |merge_request| + # The merge request data is used by PullRequestPayload and PullRequest, so we just provide it + # here. Otherwise Grape::Entity would try to access a field "payload" on Merge Request. + merge_request + end + end + end + end +end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 599d5bd0baf..1912a06682e 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -532,11 +532,29 @@ module API ::Gitlab::Tracking.event(category, action.to_s, **args) rescue => error - Rails.logger.warn( # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn( "Tracking event failed for action: #{action}, category: #{category}, 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) + return unless Gitlab::CurrentSettings.usage_ping_enabled? + + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(values, event_name) + rescue => error + Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}") + end + + def with_api_params(&block) + yield({ api: true, request: request }) + end + protected def project_finder_params_visibility_ce diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index b7ce1eba3f9..69b53ea6c2f 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -67,7 +67,7 @@ module API result == 'PONG' rescue => e - Rails.logger.warn("GitLab: An unexpected error occurred in pinging to Redis: #{e}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("GitLab: An unexpected error occurred in pinging to Redis: #{e}") false end diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb index 1161d1386bb..dcbf933a4e1 100644 --- a/lib/api/helpers/packages/conan/api_helpers.rb +++ b/lib/api/helpers/packages/conan/api_helpers.rb @@ -5,11 +5,13 @@ module API module Packages module Conan module ApiHelpers + include Gitlab::Utils::StrongMemoize + def present_download_urls(entity) authorize!(:read_package, project) presenter = ::Packages::Conan::PackagePresenter.new( - recipe, + package, current_user, project, conan_package_reference: params[:conan_package_reference] @@ -31,7 +33,7 @@ module API def recipe_upload_urls { upload_urls: Hash[ file_names.select(&method(:recipe_file?)).map do |file_name| - [file_name, recipe_file_upload_url(file_name)] + [file_name, build_recipe_file_upload_url(file_name)] end ] } end @@ -39,7 +41,7 @@ module API def package_upload_urls { upload_urls: Hash[ file_names.select(&method(:package_file?)).map do |file_name| - [file_name, package_file_upload_url(file_name)] + [file_name, build_package_file_upload_url(file_name)] end ] } end @@ -52,32 +54,58 @@ module API file_name.in?(::Packages::Conan::FileMetadatum::PACKAGE_FILES) end - def package_file_upload_url(file_name) - expose_url( - api_v4_packages_conan_v1_files_package_path( - package_name: params[:package_name], - package_version: params[:package_version], - package_username: params[:package_username], - package_channel: params[:package_channel], - recipe_revision: '0', - conan_package_reference: params[:conan_package_reference], - package_revision: '0', - file_name: file_name - ) + def build_package_file_upload_url(file_name) + options = url_options(file_name).merge( + conan_package_reference: params[:conan_package_reference], + package_revision: ::Packages::Conan::FileMetadatum::DEFAULT_PACKAGE_REVISION ) + + package_file_url(options) + end + + def build_recipe_file_upload_url(file_name) + recipe_file_url(url_options(file_name)) end - def recipe_file_upload_url(file_name) - expose_url( - api_v4_packages_conan_v1_files_export_path( - package_name: params[:package_name], - package_version: params[:package_version], - package_username: params[:package_username], - package_channel: params[:package_channel], - recipe_revision: '0', - file_name: file_name + def url_options(file_name) + { + package_name: params[:package_name], + package_version: params[:package_version], + package_username: params[:package_username], + package_channel: params[:package_channel], + file_name: file_name, + recipe_revision: ::Packages::Conan::FileMetadatum::DEFAULT_RECIPE_REVISION + } + end + + def package_file_url(options) + case package_scope + when :project + expose_url( + api_v4_projects_packages_conan_v1_files_package_path( + options.merge(id: project.id) + ) ) - ) + when :instance + expose_url( + api_v4_packages_conan_v1_files_package_path(options) + ) + end + end + + def recipe_file_url(options) + case package_scope + when :project + expose_url( + api_v4_projects_packages_conan_v1_files_export_path( + options.merge(id: project.id) + ) + ) + when :instance + expose_url( + api_v4_packages_conan_v1_files_export_path(options) + ) + end end def recipe @@ -86,16 +114,23 @@ module API def project strong_memoize(:project) do - full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username]) - Project.find_by_full_path(full_path) + case package_scope + when :project + find_project!(params[:id]) + when :instance + full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username]) + find_project!(full_path) + end end end def package strong_memoize(:package) do project.packages + .conan .with_name(params[:package_name]) .with_version(params[:package_version]) + .with_conan_username(params[:package_username]) .with_conan_channel(params[:package_channel]) .order_created .last @@ -123,7 +158,7 @@ module API conan_package_reference: params[:conan_package_reference] ).execute! - track_event('pull_package') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY + package_event('pull_package', category: 'API::ConanPackages') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY present_carrierwave_file!(package_file.file) end @@ -134,7 +169,7 @@ module API def track_push_package_event if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY && params[:file].size > 0 # rubocop: disable Style/ZeroLengthPredicate - track_event('push_package') + package_event('push_package', category: 'API::ConanPackages') end end @@ -155,6 +190,7 @@ module API def upload_package_file(file_type) authorize_upload!(project) + bad_request!('File is too large') if project.actual_limits.exceeded?(:conan_max_file_size, params['file.size'].to_i) current_package = find_or_create_package @@ -234,6 +270,10 @@ module API token end + + def package_scope + params[:id].present? ? :project : :instance + end end end end diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index c6037d52de9..403f5ea3851 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -47,6 +47,10 @@ module API authorize_create_package!(subject) require_gitlab_workhorse! end + + def package_event(event_name, **args) + track_event(event_name, **args) + end end end end diff --git a/lib/api/helpers/packages_manager_clients_helpers.rb b/lib/api/helpers/packages_manager_clients_helpers.rb index 955d21cb44f..e7662b03577 100644 --- a/lib/api/helpers/packages_manager_clients_helpers.rb +++ b/lib/api/helpers/packages_manager_clients_helpers.rb @@ -6,18 +6,8 @@ module API extend Grape::API::Helpers include ::API::Helpers::PackagesHelpers - params :workhorse_upload_params do - optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)' - optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)' - optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)' - optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)' - optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)' - optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)' - optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)' - end - def find_job_from_http_basic_auth - return unless headers + return unless request.headers token = decode_token @@ -27,7 +17,7 @@ module API end def find_deploy_token_from_http_basic_auth - return unless headers + return unless request.headers token = decode_token @@ -36,16 +26,10 @@ module API DeployToken.active.find_by_token(token) end - def uploaded_package_file(param_name = :file) - uploaded_file = UploadedFile.from_params(params, param_name, ::Packages::PackageFileUploader.workhorse_local_upload_path) - bad_request!('Missing package file!') unless uploaded_file - uploaded_file - end - private def decode_token - encoded_credentials = headers['Authorization'].to_s.split('Basic ', 2).second + encoded_credentials = request.headers['Authorization'].to_s.split('Basic ', 2).second Base64.decode64(encoded_credentials || '').split(':', 2).second end end diff --git a/lib/api/helpers/search_helpers.rb b/lib/api/helpers/search_helpers.rb index 936684ea1f8..cb5f92fa62a 100644 --- a/lib/api/helpers/search_helpers.rb +++ b/lib/api/helpers/search_helpers.rb @@ -17,6 +17,10 @@ module API # This is a separate method so that EE can redefine it. %w(issues merge_requests milestones notes wiki_blobs commits blobs users) end + + def self.search_states + %w(all opened closed merged) + end end end end diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb index ff938358439..4bceda51900 100644 --- a/lib/api/helpers/services_helpers.rb +++ b/lib/api/helpers/services_helpers.rb @@ -631,12 +631,26 @@ module API name: :issues_url, type: String, desc: 'The issues URL' + } + ], + 'ewm' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New Issue URL' }, { - required: false, - name: :description, + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: true, + name: :issues_url, type: String, - desc: 'The description of the tracker' + desc: 'Issues URL' } ], 'youtrack' => [ @@ -651,12 +665,6 @@ module API name: :issues_url, type: String, desc: 'The issues URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'The description of the tracker' } ], 'slack' => [ @@ -747,6 +755,7 @@ module API ::DiscordService, ::DroneCiService, ::EmailsOnPushService, + ::EwmService, ::ExternalWikiService, ::FlowdockService, ::HangoutsChatService, diff --git a/lib/api/helpers/snippets_helpers.rb b/lib/api/helpers/snippets_helpers.rb index 79367da8d1f..9224381735f 100644 --- a/lib/api/helpers/snippets_helpers.rb +++ b/lib/api/helpers/snippets_helpers.rb @@ -27,6 +27,24 @@ module API exactly_one_of :files, :content end + params :update_file_params do |options| + optional :files, type: Array, desc: 'An array of files to update' do + requires :action, type: String, + values: SnippetInputAction::ACTIONS.map(&:to_s), + desc: "The type of action to perform on the file, must be one of: #{SnippetInputAction::ACTIONS.join(", ")}" + optional :content, type: String, desc: 'The content of a snippet' + optional :file_path, file_path: true, type: String, desc: 'The file path of a snippet file' + optional :previous_path, file_path: true, type: String, desc: 'The previous path of a snippet file' + end + + mutually_exclusive :files, :content + mutually_exclusive :files, :file_name + end + + params :minimum_update_params do + at_least_one_of :content, :description, :files, :file_name, :title, :visibility + end + def content_for(snippet) if snippet.empty_repo? env['api.format'] = :txt @@ -53,10 +71,30 @@ module API end end - def process_file_args(args) - args[:snippet_actions] = args.delete(:files)&.map do |file| - file[:action] = :create - file.symbolize_keys + def process_create_params(args) + with_api_params do |api_params| + args[:snippet_actions] = args.delete(:files)&.map do |file| + file[:action] = :create + file.symbolize_keys + end + + args.merge(api_params) + end + end + + def process_update_params(args) + with_api_params do |api_params| + args[:snippet_actions] = args.delete(:files)&.map(&:symbolize_keys) + + args.merge(api_params) + end + end + + def validate_params_for_multiple_files(snippet) + return unless params[:content] || params[:file_name] + + if Feature.enabled?(:snippet_multiple_files, current_user) && snippet.multiple_files? + render_api_error!({ error: _('To update Snippets with multiple files, you must use the `files` parameter') }, 400) end end end diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index 17599c72243..ff687a57888 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -241,14 +241,16 @@ module API break { success: false, message: "Invalid token expiry date: '#{params[:expires_at]}'" } end - access_token = nil + result = ::PersonalAccessTokens::CreateService.new( + user, name: params[:name], scopes: params[:scopes], expires_at: expires_at + ).execute - ::Users::UpdateService.new(current_user, user: user).execute! do |user| - access_token = user.personal_access_tokens.create!( - name: params[:name], scopes: params[:scopes], expires_at: expires_at - ) + unless result.status == :success + break { success: false, message: "Failed to create token: #{result.message}" } end + access_token = result.payload[:personal_access_token] + { success: true, token: access_token.token, scopes: access_token.scopes, expires_at: access_token.expires_at } end diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb index 7f64fd7efe3..6d5dfd086e7 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -4,7 +4,16 @@ module API # Kubernetes Internal API module Internal class Kubernetes < Grape::API::Instance + before do + check_feature_enabled + authenticate_gitlab_kas_request! + end + helpers do + def authenticate_gitlab_kas_request! + unauthorized! unless Gitlab::Kas.verify_api_request(headers) + end + def agent_token @agent_token ||= cluster_agent_token_from_authorization_token end @@ -36,7 +45,7 @@ module API end def check_feature_enabled - not_found! unless Feature.enabled?(:kubernetes_agent_internal_api) + not_found! unless Feature.enabled?(:kubernetes_agent_internal_api, default_enabled: true, type: :ops) end def check_agent_token @@ -47,7 +56,6 @@ module API namespace 'internal' do namespace 'kubernetes' do before do - check_feature_enabled check_agent_token end @@ -89,6 +97,26 @@ module API } end end + + namespace 'kubernetes/usage_metrics' do + desc 'POST usage metrics' do + detail 'Updates usage metrics for agent' + end + params do + requires :gitops_sync_count, type: Integer, desc: 'The count to increment the gitops_sync metric by' + end + post '/' do + gitops_sync_count = params[:gitops_sync_count] + + if gitops_sync_count < 0 + bad_request!('gitops_sync_count must be greater than or equal to zero') + else + Gitlab::UsageDataCounters::KubernetesAgentCounter.increment_gitops_sync(gitops_sync_count) + + no_content! + end + end + end end end end diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb new file mode 100644 index 00000000000..6cc5b344f47 --- /dev/null +++ b/lib/api/issue_links.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module API + class IssueLinks < Grape::API::Instance + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Get related issues' do + success Entities::RelatedIssue + end + get ':id/issues/:issue_iid/links' do + source_issue = find_project_issue(params[:issue_iid]) + related_issues = source_issue.related_issues(current_user) + + present related_issues, + with: Entities::RelatedIssue, + current_user: current_user, + project: user_project + end + + desc 'Relate issues' do + success Entities::IssueLink + end + params do + requires :target_project_id, type: String, desc: 'The ID of the target project' + requires :target_issue_iid, type: Integer, desc: 'The IID of the target issue' + optional :link_type, type: String, values: IssueLink.link_types.keys, + desc: 'The type of the relation' + end + # rubocop: disable CodeReuse/ActiveRecord + post ':id/issues/:issue_iid/links' do + source_issue = find_project_issue(params[:issue_iid]) + target_issue = find_project_issue(declared_params[:target_issue_iid], + declared_params[:target_project_id]) + + create_params = { target_issuable: target_issue, link_type: declared_params[:link_type] } + + result = ::IssueLinks::CreateService + .new(source_issue, current_user, create_params) + .execute + + if result[:status] == :success + issue_link = IssueLink.find_by!(source: source_issue, target: target_issue) + + present issue_link, with: Entities::IssueLink + else + render_api_error!(result[:message], result[:http_status]) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + desc 'Remove issues relation' do + success Entities::IssueLink + end + params do + requires :issue_link_id, type: Integer, desc: 'The ID of an issue link' + end + delete ':id/issues/:issue_iid/links/:issue_link_id' do + issue_link = IssueLink.find(declared_params[:issue_link_id]) + + find_project_issue(params[:issue_iid]) + find_project_issue(issue_link.target.iid.to_s, issue_link.target.project_id.to_s) + + result = ::IssueLinks::DestroyService + .new(issue_link, current_user) + .execute + + if result[:status] == :success + present issue_link, with: Entities::IssueLink + else + render_api_error!(result[:message], result[:http_status]) + end + end + end + end +end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 1694a967f26..0e5b0fae6e2 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -114,6 +114,19 @@ module API present issues, options end + + desc "Get specified issue (admin only)" do + success Entities::Issue + end + params do + requires :id, type: String, desc: 'The ID of the Issue' + end + get ":id" do + authenticated_as_admin! + issue = Issue.find(params['id']) + + present issue, with: Entities::Issue, current_user: current_user, project: issue.project + end end params do diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 084c146abe7..ad46d948f3b 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -48,54 +48,6 @@ module API end # rubocop: enable CodeReuse/ActiveRecord - desc 'Get pipeline jobs' do - success Entities::Ci::Job - end - params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' - use :optional_scope - use :pagination - end - # rubocop: disable CodeReuse/ActiveRecord - get ':id/pipelines/:pipeline_id/jobs' do - authorize!(:read_pipeline, user_project) - pipeline = user_project.all_pipelines.find(params[:pipeline_id]) - authorize!(:read_build, pipeline) - - builds = pipeline.builds - builds = filter_builds(builds, params[:scope]) - builds = builds.preload(:job_artifacts_archive, :job_artifacts, project: [:namespace]) - - present paginate(builds), with: Entities::Ci::Job - end - # rubocop: enable CodeReuse/ActiveRecord - - desc 'Get pipeline bridge jobs' do - success ::API::Entities::Ci::Bridge - end - params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' - use :optional_scope - use :pagination - end - # rubocop: disable CodeReuse/ActiveRecord - get ':id/pipelines/:pipeline_id/bridges' do - authorize!(:read_build, user_project) - pipeline = user_project.ci_pipelines.find(params[:pipeline_id]) - authorize!(:read_pipeline, pipeline) - - bridges = pipeline.bridges - bridges = filter_builds(bridges, params[:scope]) - bridges = bridges.preload( - :metadata, - downstream_pipeline: [project: [:route, { namespace: :route }]], - project: [:namespace] - ) - - present paginate(bridges), with: ::API::Entities::Ci::Bridge - end - # rubocop: enable CodeReuse/ActiveRecord - desc 'Get a specific job of a project' do success Entities::Ci::Job end diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb index 32a45c59cfa..e6d9a9a7c20 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -107,7 +107,7 @@ module API when 'sha1' package_file.file_sha1 else - track_event('pull_package') if jar_file?(format) + package_event('pull_package') if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end end @@ -145,7 +145,7 @@ module API when 'sha1' package_file.file_sha1 else - track_event('pull_package') if jar_file?(format) + package_event('pull_package') if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end @@ -181,7 +181,7 @@ module API when 'sha1' package_file.file_sha1 else - track_event('pull_package') if jar_file?(format) + package_event('pull_package') if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end @@ -200,7 +200,7 @@ module API status 200 content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE - ::Packages::PackageFileUploader.workhorse_authorize(has_length: true) + ::Packages::PackageFileUploader.workhorse_authorize(has_length: true, maximum_size: user_project.actual_limits.maven_max_file_size) end desc 'Upload the maven package file' do @@ -214,6 +214,7 @@ module API route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do authorize_upload! + bad_request!('File is too large') if user_project.actual_limits.exceeded?(:maven_max_file_size, params[:file].size) file_name, format = extract_format(params[:file_name]) @@ -232,7 +233,7 @@ module API when 'md5' nil else - track_event('push_package') if jar_file?(format) + package_event('push_package') if jar_file?(format) file_params = { file: params[:file], diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 6f25df720c4..4bd72b267a9 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -29,11 +29,13 @@ module API remove_labels milestone_id remove_source_branch - state_event + allow_collaboration + allow_maintainer_to_push + squash target_branch title + state_event discussion_locked - squash ] end @@ -154,13 +156,13 @@ module API helpers do params :optional_params do - optional :description, type: String, desc: 'The description of the merge request' optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' optional :assignee_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The array of user IDs to assign issue' - optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' + optional :description, type: String, desc: 'The description of the merge request' optional :labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :add_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :remove_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' + optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch' optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration' diff --git a/lib/api/npm_packages.rb b/lib/api/npm_packages.rb index 21ca57b7985..fca405b76b7 100644 --- a/lib/api/npm_packages.rb +++ b/lib/api/npm_packages.rb @@ -141,7 +141,7 @@ module API package_file = ::Packages::PackageFileFinder .new(package, params[:file_name]).execute! - track_event('pull_package') + package_event('pull_package') present_carrierwave_file!(package_file.file) end @@ -157,7 +157,7 @@ module API put ':id/packages/npm/:package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do authorize_create_package!(user_project) - track_event('push_package') + package_event('push_package') created_package = ::Packages::Npm::CreatePackageService .new(user_project, current_user, params.merge(build: current_authenticated_job)).execute diff --git a/lib/api/nuget_packages.rb b/lib/api/nuget_packages.rb index 56c4de2071d..f84a3acbe6d 100644 --- a/lib/api/nuget_packages.rb +++ b/lib/api/nuget_packages.rb @@ -92,6 +92,7 @@ module API 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], @@ -104,7 +105,7 @@ module API package_file = ::Packages::CreatePackageFileService.new(package, file_params) .execute - track_event('push_package') + package_event('push_package') ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker @@ -118,7 +119,11 @@ module API 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) + authorize_workhorse!( + subject: authorized_user_project, + has_length: false, + maximum_size: authorized_user_project.actual_limits.nuget_max_file_size + ) end params do @@ -193,7 +198,7 @@ module API not_found!('Package') unless package_file - track_event('pull_package') + package_event('pull_package') # 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) @@ -228,7 +233,7 @@ module API .new(authorized_user_project, params[:q], search_options) .execute - track_event('search_package') + package_event('search_package') present ::Packages::Nuget::SearchResultsPresenter.new(search), with: ::API::Entities::Nuget::SearchResults diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index fba4c60504f..f6e87fece89 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -64,12 +64,8 @@ module API end post ":id/snippets" do authorize! :create_snippet, user_project - snippet_params = declared_params(include_missing: false).tap do |create_args| - create_args[:request] = request - create_args[:api] = true - process_file_args(create_args) - end + snippet_params = process_create_params(declared_params(include_missing: false)) service_response = ::Snippets::CreateService.new(user_project, current_user, snippet_params).execute snippet = service_response.payload[:snippet] @@ -88,14 +84,16 @@ module API end params do requires :snippet_id, type: Integer, desc: 'The ID of a project snippet' - optional :title, type: String, allow_blank: false, desc: 'The title of the snippet' - optional :file_name, type: String, desc: 'The file name of the snippet' optional :content, type: String, allow_blank: false, desc: 'The content of the snippet' optional :description, type: String, desc: 'The description of a snippet' + optional :file_name, type: String, desc: 'The file name of the snippet' + optional :title, type: String, allow_blank: false, desc: 'The title of the snippet' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' - at_least_one_of :title, :file_name, :content, :visibility_level + + use :update_file_params + use :minimum_update_params end # rubocop: disable CodeReuse/ActiveRecord put ":id/snippets/:snippet_id" do @@ -104,8 +102,9 @@ module API authorize! :update_snippet, snippet - snippet_params = declared_params(include_missing: false) - .merge(request: request, api: true) + validate_params_for_multiple_files(snippet) + + snippet_params = process_update_params(declared_params(include_missing: false)) service_response = ::Snippets::UpdateService.new(user_project, current_user, snippet_params).execute(snippet) snippet = service_response.payload[:snippet] diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index 739928a61ed..c07db68f8a8 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -64,7 +64,7 @@ module API requires :sha256, type: String, desc: 'The PyPi package sha256 check sum' end - route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth get 'files/:sha256/*file_identifier' do project = unauthorized_user_project! @@ -72,7 +72,7 @@ module API package = packages_finder(project).by_file_name_and_sha256(filename, params[:sha256]) package_file = ::Packages::PackageFileFinder.new(package, filename, with_file_name_like: false).execute - track_event('pull_package') + package_event('pull_package') present_carrierwave_file!(package_file.file, supports_direct_download: true) end @@ -87,11 +87,11 @@ module API # An Api entry point but returns an HTML file instead of JSON. # PyPi simple API returns the package descriptor as a simple HTML file. - route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth get 'simple/*package_name', format: :txt do authorize_read_package!(authorized_user_project) - track_event('list_package') + package_event('list_package') packages = find_package_versions presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project) @@ -117,11 +117,12 @@ module API optional :sha256_digest, type: String end - route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth post do authorize_upload!(authorized_user_project) + bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:pypi_max_file_size, params[:content].size) - track_event('push_package') + package_event('push_package') ::Packages::Pypi::CreatePackageService .new(authorized_user_project, current_user, declared_params) @@ -134,9 +135,13 @@ module API forbidden! end - route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth post 'authorize' do - authorize_workhorse!(subject: authorized_user_project, has_length: false) + authorize_workhorse!( + subject: authorized_user_project, + has_length: false, + maximum_size: authorized_user_project.actual_limits.pypi_max_file_size + ) end end end diff --git a/lib/api/search.rb b/lib/api/search.rb index 53095e0b81a..b9c6a823f4f 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -32,6 +32,7 @@ module API search_params = { scope: params[:scope], search: params[:search], + state: params[:state], snippets: snippets?, page: params[:page], per_page: params[:per_page] @@ -79,6 +80,7 @@ module API type: String, desc: 'The scope of the search', values: Helpers::SearchHelpers.global_search_scopes + optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states use :pagination end get do @@ -100,6 +102,7 @@ module API type: String, desc: 'The scope of the search', values: Helpers::SearchHelpers.group_search_scopes + optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states use :pagination end get ':id/(-/)search' do @@ -122,6 +125,7 @@ module API desc: 'The scope of the search', values: Helpers::SearchHelpers.project_search_scopes optional :ref, type: String, desc: 'The name of a repository branch or tag. If not given, the default branch is used' + optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states use :pagination end get ':id/(-/)search' do diff --git a/lib/api/settings.rb b/lib/api/settings.rb index f2e0aaecfb9..6e5534d0c9a 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -61,6 +61,10 @@ module API end optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.' optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.' + optional :gitpod_enabled, type: Boolean, desc: 'Enable Gitpod' + given gitpod_enabled: ->(val) { val } do + requires :gitpod_url, type: String, desc: 'The configured Gitpod instance URL' + end optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.' @@ -140,11 +144,9 @@ module API end optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.' - optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins' optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated' optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5 optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking' - optional :snowplow_iglu_registry_url, type: String, desc: 'The Snowplow base Iglu Schema Registry URL to use for custom context and self describing events' given snowplow_enabled: ->(val) { val } do requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname' optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain' @@ -152,6 +154,7 @@ module API end optional :issues_create_limit, type: Integer, desc: "Maximum number of issue creation requests allowed per minute per user. Set to 0 for unlimited requests per minute." optional :raw_blob_request_limit, type: Integer, desc: "Maximum number of requests per minute for each raw path. Set to 0 for unlimited requests per minute." + optional :wiki_page_max_content_bytes, type: Integer, desc: "Maximum wiki page content size in bytes" ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb index de1373144e3..77f2b1e871e 100644 --- a/lib/api/sidekiq_metrics.rb +++ b/lib/api/sidekiq_metrics.rb @@ -17,7 +17,7 @@ module API end def process_metrics - Sidekiq::ProcessSet.new.map do |process| + Sidekiq::ProcessSet.new(false).map do |process| { hostname: process['hostname'], pid: process['pid'], diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index 1a3283aed98..c6ef35875fc 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -76,12 +76,7 @@ module API post do authorize! :create_snippet - attrs = declared_params(include_missing: false).tap do |create_args| - create_args[:request] = request - create_args[:api] = true - - process_file_args(create_args) - end + attrs = process_create_params(declared_params(include_missing: false)) service_response = ::Snippets::CreateService.new(nil, current_user, attrs).execute snippet = service_response.payload[:snippet] @@ -99,16 +94,19 @@ module API detail 'This feature was introduced in GitLab 8.15.' success Entities::PersonalSnippet end + params do requires :id, type: Integer, desc: 'The ID of a snippet' - optional :title, type: String, allow_blank: false, desc: 'The title of a snippet' - optional :file_name, type: String, desc: 'The name of a snippet file' optional :content, type: String, allow_blank: false, desc: 'The content of a snippet' optional :description, type: String, desc: 'The description of a snippet' + optional :file_name, type: String, desc: 'The name of a snippet file' + optional :title, type: String, allow_blank: false, desc: 'The title of a snippet' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' - at_least_one_of :title, :file_name, :content, :visibility + + use :update_file_params + use :minimum_update_params end put ':id' do snippet = snippets_for_current_user.find_by_id(params.delete(:id)) @@ -116,8 +114,12 @@ module API authorize! :update_snippet, snippet - attrs = declared_params(include_missing: false).merge(request: request, api: true) + validate_params_for_multiple_files(snippet) + + attrs = process_update_params(declared_params(include_missing: false)) + service_response = ::Snippets::UpdateService.new(nil, current_user, attrs).execute(snippet) + snippet = service_response.payload[:snippet] if service_response.success? diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb index f6e966defce..7063a3d08b5 100644 --- a/lib/api/terraform/state.rb +++ b/lib/api/terraform/state.rb @@ -35,10 +35,10 @@ module API route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth get do remote_state_handler.find_with_lock do |state| - no_content! unless state.file.exists? + no_content! unless state.latest_file && state.latest_file.exists? env['api.format'] = :binary # this bypasses json serialization - body state.file.read + body state.latest_file.read status :ok end end @@ -52,8 +52,7 @@ module API no_content! if data.empty? remote_state_handler.handle_with_lock do |state| - state.file = CarrierWaveStringFile.new(data) - state.save! + state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial]) status :ok end end diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 4a73e3e0e94..5eae92a251e 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -39,8 +39,17 @@ module API resource :todos do helpers do + params :todo_filters do + optional :action, String, values: Todo::ACTION_NAMES.values.map(&:to_s) + optional :author_id, Integer + optional :state, String, values: Todo.state_machine.states.map(&:name).map(&:to_s) + optional :type, String, values: TodosFinder.todo_types + optional :project_id, Integer + optional :group_id, Integer + end + def find_todos - TodosFinder.new(current_user, params).execute + TodosFinder.new(current_user, declared_params(include_missing: false)).execute end def issuable_and_awardable?(type) @@ -72,7 +81,7 @@ module API success Entities::Todo end params do - use :pagination + use :pagination, :todo_filters end get do todos = paginate(find_todos.with_entity_associations) diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb new file mode 100644 index 00000000000..a1512197ee1 --- /dev/null +++ b/lib/api/usage_data.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module API + class UsageData < Grape::API::Instance + before { authenticate! } + + namespace 'usage_data' do + before do + not_found! unless Feature.enabled?(:usage_data_api) + forbidden!('Invalid CSRF token is provided') unless verified_request? + end + + desc 'Track usage data events' do + detail 'This feature was introduced in GitLab 13.4.' + 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] + + increment_unique_values(event_name, current_user.id) + + status :ok + end + end + end +end diff --git a/lib/api/users.rb b/lib/api/users.rb index 335624963aa..73bb43b88fc 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -84,6 +84,7 @@ module API optional :created_after, type: DateTime, desc: 'Return users created after the specified time' optional :created_before, type: DateTime, desc: 'Return users created before the specified time' optional :without_projects, type: Boolean, default: false, desc: 'Filters only users without projects' + optional :exclude_internal, as: :non_internal, type: Boolean, default: false, desc: 'Filters only non internal users' all_or_none_of :extern_uid, :provider use :sort_params @@ -115,6 +116,7 @@ module API entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin + users = users.preload(:identities, :webauthn_registrations) if entity == Entities::UserWithAdmin users, options = with_custom_attributes(users, { with: entity, current_user: current_user }) users = users.preload(:user_detail) @@ -217,9 +219,15 @@ module API .where.not(id: user.id).exists? user_params = declared_params(include_missing: false) + admin_making_changes_for_another_user = (current_user != user) - user_params[:password_expires_at] = Time.current if user_params[:password].present? - result = ::Users::UpdateService.new(current_user, user_params.merge(user: user)).execute + if user_params[:password].present? + user_params[:password_expires_at] = Time.current if admin_making_changes_for_another_user + end + + result = ::Users::UpdateService.new(current_user, user_params.merge(user: user)).execute do |user| + user.send_only_admin_changed_your_password_notification! if admin_making_changes_for_another_user + end if result[:status] == :success present user, with: Entities::UserWithAdmin, current_user: current_user diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb new file mode 100644 index 00000000000..593f90460ac --- /dev/null +++ b/lib/api/v3/github.rb @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +# These endpoints partially mimic Github API behavior in order to successfully +# integrate with Jira Development Panel. +# Endpoints returning an empty list were temporarily added to avoid 404's +# during Jira's DVCS integration. +# +module API + module V3 + class Github < Grape::API::Instance + NO_SLASH_URL_PART_REGEX = %r{[^/]+}.freeze + ENDPOINT_REQUIREMENTS = { + namespace: NO_SLASH_URL_PART_REGEX, + project: NO_SLASH_URL_PART_REGEX, + username: NO_SLASH_URL_PART_REGEX + }.freeze + + # Used to differentiate Jira Cloud requests from Jira Server requests + # Jira Cloud user agent format: Jira DVCS Connector Vertigo/version + # Jira Server user agent format: Jira DVCS Connector/version + JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo'.freeze + + include PaginationParams + + before do + authorize_jira_user_agent!(request) + authenticate! + end + + helpers do + params :project_full_path do + requires :namespace, type: String + requires :project, type: String + end + + def authorize_jira_user_agent!(request) + not_found! unless Gitlab::Jira::Middleware.jira_dvcs_connector?(request.env) + end + + def update_project_feature_usage_for(project) + # Prevent errors on GitLab Geo not allowing + # UPDATE statements to happen in GET requests. + return if Gitlab::Database.read_only? + + project.log_jira_dvcs_integration_usage(cloud: jira_cloud?) + end + + def jira_cloud? + request.env['HTTP_USER_AGENT'].include?(JIRA_DVCS_CLOUD_USER_AGENT) + end + + def find_project_with_access(params) + project = find_project!( + ::Gitlab::Jira::Dvcs.restore_full_path(params.slice(:namespace, :project).symbolize_keys) + ) + not_found! unless can?(current_user, :download_code, project) + project + end + + # rubocop: disable CodeReuse/ActiveRecord + def find_merge_requests + merge_requests = authorized_merge_requests.reorder(updated_at: :desc) + paginate(merge_requests) + end + # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def find_merge_request_with_access(id, access_level = :read_merge_request) + merge_request = authorized_merge_requests.find_by(id: id) + not_found! unless can?(current_user, access_level, merge_request) + merge_request + end + # rubocop: enable CodeReuse/ActiveRecord + + def authorized_merge_requests + MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?).execute + end + + def authorized_merge_requests_for_project(project) + MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?, project_id: project.id).execute + end + + # rubocop: disable CodeReuse/ActiveRecord + def find_notes(noteable) + # They're not presented on Jira Dev Panel ATM. A comments count with a + # redirect link is presented. + notes = paginate(noteable.notes.user.reorder(nil)) + notes.select { |n| n.readable_by?(current_user) } + end + # rubocop: enable CodeReuse/ActiveRecord + end + + resource :orgs do + get ':namespace/repos' do + present [] + end + end + + resource :user do + get :repos do + present [] + end + end + + resource :users do + params do + use :pagination + end + + get ':namespace/repos' do + namespace = Namespace.find_by_full_path(params[:namespace]) + not_found!('Namespace') unless namespace + + projects = current_user.can_read_all_resources? ? Project.all : current_user.authorized_projects + projects = projects.in_namespace(namespace.self_and_descendants) + + projects_cte = Project.wrap_with_cte(projects) + .eager_load_namespace_and_owner + .with_route + + present paginate(projects_cte), + with: ::API::Github::Entities::Repository, + root_namespace: namespace.root_ancestor + end + + get ':username' do + forbidden! unless can?(current_user, :read_users_list) + user = UsersFinder.new(current_user, { username: params[:username] }).execute.first + not_found! unless user + present user, with: ::API::Github::Entities::User + end + end + + # Jira dev panel integration weirdly requests for "/-/jira/pulls" instead + # "/api/v3/repos/<namespace>/<project>/pulls". This forces us into + # returning _all_ Merge Requests from authorized projects (user is a member), + # instead just the authorized MRs from a project. + # Jira handles the filtering, presenting just MRs mentioning the Jira + # issue ID on the MR title / description. + resource :repos do + # Keeping for backwards compatibility with old Jira integration instructions + # so that users that do not change it will not suddenly have a broken integration + get '/-/jira/pulls' do + present find_merge_requests, with: ::API::Github::Entities::PullRequest + end + + get '/-/jira/events' do + present [] + end + + params do + use :project_full_path + end + get ':namespace/:project/pulls' do + user_project = find_project_with_access(params) + + merge_requests = authorized_merge_requests_for_project(user_project) + + present paginate(merge_requests), with: ::API::Github::Entities::PullRequest + end + + params do + use :project_full_path + end + get ':namespace/:project/pulls/:id' do + merge_request = find_merge_request_with_access(params[:id]) + + present merge_request, with: ::API::Github::Entities::PullRequest + end + + # In Github, each Merge Request is automatically also an issue. + # Therefore we return its comments here. + # It'll present _just_ the comments counting with a link to GitLab on + # Jira dev panel, not the actual note content. + get ':namespace/:project/issues/:id/comments' do + merge_request = find_merge_request_with_access(params[:id]) + + present find_notes(merge_request), with: ::API::Github::Entities::NoteableComment + end + + # This refer to "review" comments but Jira dev panel doesn't seem to + # present it accordingly. + get ':namespace/:project/pulls/:id/comments' do + present [] + end + + # Commits are not presented within "Pull Requests" modal on Jira dev + # panel. + get ':namespace/:project/pulls/:id/commits' do + present [] + end + + # Self-hosted Jira (tested on 7.11.1) requests this endpoint right + # after fetching branches. + get ':namespace/:project/events' do + user_project = find_project_with_access(params) + + merge_requests = authorized_merge_requests_for_project(user_project) + + present paginate(merge_requests), with: ::API::Github::Entities::PullRequestEvent + end + + params do + use :project_full_path + use :pagination + end + get ':namespace/:project/branches' do + user_project = find_project_with_access(params) + + update_project_feature_usage_for(user_project) + + branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name)) + + present paginate(branches), with: ::API::Github::Entities::Branch, project: user_project + end + + params do + use :project_full_path + end + get ':namespace/:project/commits/:sha' do + user_project = find_project_with_access(params) + + commit = user_project.commit(params[:sha]) + + not_found! 'Commit' unless commit + + present commit, with: ::API::Github::Entities::RepoCommit + end + end + end + end +end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index cea0cb3a19c..0b3ec10f1b4 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -17,7 +17,6 @@ module API def find_variable(params) variables = ::Ci::VariablesFinder.new(user_project, params).execute.to_a - return variables.first unless ::Gitlab::Ci::Features.variables_api_filter_environment_scope? return variables.first unless variables.many? # rubocop: disable CodeReuse/ActiveRecord conflict!("There are multiple variables with provided parameters. Please use 'filter[environment_scope]'") diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 95afa36113c..4eba12157bd 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -83,11 +83,12 @@ module API put ':id/wikis/:slug' do authorize! :create_wiki, container - page = WikiPages::UpdateService + response = WikiPages::UpdateService .new(container: container, current_user: current_user, params: params) .execute(wiki_page) + page = response.payload[:page] - if page.valid? + if response.success? present page, with: Entities::WikiPage else render_validation_error!(page) @@ -101,11 +102,15 @@ module API delete ':id/wikis/:slug' do authorize! :admin_wiki, container - WikiPages::DestroyService + response = WikiPages::DestroyService .new(container: container, current_user: current_user) .execute(wiki_page) - no_content! + if response.success? + no_content! + else + render_api_error!(reponse.message) + end end desc 'Upload an attachment to the wiki repository' do diff --git a/lib/atlassian/jira_connect.rb b/lib/atlassian/jira_connect.rb new file mode 100644 index 00000000000..7f693eff59b --- /dev/null +++ b/lib/atlassian/jira_connect.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + class << self + def app_name + "GitLab for Jira (#{gitlab_host})" + end + + def app_key + "gitlab-jira-connect-#{gitlab_host}" + end + + private + + def gitlab_host + Gitlab.config.gitlab.host + end + end + end +end diff --git a/lib/atlassian/jira_connect/client.rb b/lib/atlassian/jira_connect/client.rb new file mode 100644 index 00000000000..0b578c03782 --- /dev/null +++ b/lib/atlassian/jira_connect/client.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + class Client < Gitlab::HTTP + def initialize(base_uri, shared_secret) + @base_uri = base_uri + @shared_secret = shared_secret + end + + def store_dev_info(project:, commits: nil, branches: nil, merge_requests: nil) + dev_info_json = { + repositories: [ + Serializers::RepositoryEntity.represent( + project, + commits: commits, + branches: branches, + merge_requests: merge_requests + ) + ] + }.to_json + + uri = URI.join(@base_uri, '/rest/devinfo/0.10/bulk') + + headers = { + 'Authorization' => "JWT #{jwt_token('POST', uri)}", + 'Content-Type' => 'application/json' + } + + self.class.post(uri, headers: headers, body: dev_info_json) + end + + private + + def jwt_token(http_method, uri) + claims = Atlassian::Jwt.build_claims( + Atlassian::JiraConnect.app_key, + uri, + http_method, + @base_uri + ) + + Atlassian::Jwt.encode(claims, @shared_secret) + end + end + end +end diff --git a/lib/atlassian/jira_connect/serializers/author_entity.rb b/lib/atlassian/jira_connect/serializers/author_entity.rb new file mode 100644 index 00000000000..9ab8e34c14b --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/author_entity.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + class AuthorEntity < Grape::Entity + include Gitlab::Routing + + expose :name + expose :email + + with_options(unless: -> (user) { user.is_a?(CommitEntity::CommitAuthor) }) do + expose :username + expose :url do |user| + user_url(user) + end + expose :avatar do |user| + user.avatar_url(only_path: false) + end + end + end + end + end +end diff --git a/lib/atlassian/jira_connect/serializers/base_entity.rb b/lib/atlassian/jira_connect/serializers/base_entity.rb new file mode 100644 index 00000000000..c5490aa3f54 --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/base_entity.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + class BaseEntity < Grape::Entity + include Gitlab::Routing + include GitlabRoutingHelper + + format_with(:string) { |value| value.to_s } + + expose :monotonic_time, as: :updateSequenceId + + private + + def monotonic_time + Gitlab::Metrics::System.monotonic_time.to_i + end + end + end + end +end diff --git a/lib/atlassian/jira_connect/serializers/branch_entity.rb b/lib/atlassian/jira_connect/serializers/branch_entity.rb new file mode 100644 index 00000000000..c663575b7a8 --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/branch_entity.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + class BranchEntity < BaseEntity + expose :id do |branch| + Digest::SHA256.hexdigest(branch.name) + end + expose :issueKeys do |branch| + JiraIssueKeyExtractor.new(branch.name).issue_keys + end + expose :name + expose :lastCommit, using: JiraConnect::Serializers::CommitEntity do |branch, options| + options[:project].commit(branch.dereferenced_target) + end + + expose :url do |branch, options| + project_commits_url(options[:project], branch.name) + end + expose :createPullRequestUrl do |branch, options| + project_new_merge_request_url( + options[:project], + merge_request: { + source_branch: branch.name + } + ) + end + end + end + end +end diff --git a/lib/atlassian/jira_connect/serializers/commit_entity.rb b/lib/atlassian/jira_connect/serializers/commit_entity.rb new file mode 100644 index 00000000000..12eb1ed15ea --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/commit_entity.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + class CommitEntity < BaseEntity + CommitAuthor = Struct.new(:name, :email) + + expose :id + expose :issueKeys do |commit| + JiraIssueKeyExtractor.new(commit.safe_message).issue_keys + end + expose :id, as: :hash + expose :short_id, as: :displayId + expose :safe_message, as: :message + expose :flags do |commit| + if commit.merge_commit? + ['MERGE_COMMIT'] + else + [] + end + end + expose :author, using: JiraConnect::Serializers::AuthorEntity + expose :fileCount do |commit| + commit.stats.total + end + expose :files do |commit, options| + files = commit.diffs(max_files: 10).diff_files + JiraConnect::Serializers::FileEntity.represent files, options.merge(commit: commit) + end + expose :created_at, as: :authorTimestamp + + expose :url do |commit, options| + project_commit_url(options[:project], commit.id) + end + + private + + def author + object.author || CommitAuthor.new(object.author_name, object.author_email) + end + end + end + end +end diff --git a/lib/atlassian/jira_connect/serializers/file_entity.rb b/lib/atlassian/jira_connect/serializers/file_entity.rb new file mode 100644 index 00000000000..50d31965f93 --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/file_entity.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + class FileEntity < Grape::Entity + include Gitlab::Routing + + expose :path do |file| + file.deleted_file? ? file.old_path : file.new_path + end + expose :changeType do |file| + if file.new_file? + 'ADDED' + elsif file.deleted_file? + 'DELETED' + elsif file.renamed_file? + 'MOVED' + else + 'MODIFIED' + end + end + expose :added_lines, as: :linesAdded + expose :removed_lines, as: :linesRemoved + + expose :url do |file, options| + file_path = if file.deleted_file? + File.join(options[:commit].parent_id, file.old_path) + else + File.join(options[:commit].id, file.new_path) + end + + project_blob_url(options[:project], file_path) + end + end + end + end +end diff --git a/lib/atlassian/jira_connect/serializers/pull_request_entity.rb b/lib/atlassian/jira_connect/serializers/pull_request_entity.rb new file mode 100644 index 00000000000..0ddfcbf52ea --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/pull_request_entity.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + class PullRequestEntity < BaseEntity + STATUS_MAPPING = { + 'opened' => 'OPEN', + 'locked' => 'OPEN', + 'merged' => 'MERGED', + 'closed' => 'DECLINED' + }.freeze + + expose :id, format_with: :string + expose :issueKeys do |mr| + JiraIssueKeyExtractor.new(mr.title, mr.description).issue_keys + end + expose :displayId do |mr| + mr.to_reference(full: true) + end + expose :title + expose :author, using: JiraConnect::Serializers::AuthorEntity + expose :user_notes_count, as: :commentCount + expose :source_branch, as: :sourceBranch + expose :target_branch, as: :destinationBranch + expose :lastUpdate do |mr| + mr.last_edited_at || mr.created_at + end + expose :status do |mr| + STATUS_MAPPING[mr.state] || 'UNKNOWN' + end + + expose :sourceBranchUrl do |mr| + project_commits_url(mr.project, mr.source_branch) + end + expose :url do |mr| + merge_request_url(mr) + end + end + end + end +end diff --git a/lib/atlassian/jira_connect/serializers/repository_entity.rb b/lib/atlassian/jira_connect/serializers/repository_entity.rb new file mode 100644 index 00000000000..819ca2b62e0 --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/repository_entity.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + class RepositoryEntity < BaseEntity + expose :id, format_with: :string + expose :name + expose :description + expose :url do |project| + project_url(project) + end + expose :avatar do |project| + project.avatar_url(only_path: false) + end + + expose :commits do |project, options| + JiraConnect::Serializers::CommitEntity.represent options[:commits], project: project + end + expose :branches do |project, options| + JiraConnect::Serializers::BranchEntity.represent options[:branches], project: project + end + expose :pullRequests do |project, options| + JiraConnect::Serializers::PullRequestEntity.represent options[:merge_requests], project: project + end + end + end + end +end diff --git a/lib/atlassian/jira_issue_key_extractor.rb b/lib/atlassian/jira_issue_key_extractor.rb new file mode 100644 index 00000000000..f1b432787ac --- /dev/null +++ b/lib/atlassian/jira_issue_key_extractor.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Atlassian + class JiraIssueKeyExtractor + def self.has_keys?(*text) + new(*text).issue_keys.any? + end + + def initialize(*text) + @text = text.join(' ') + end + + def issue_keys + @text.scan(Gitlab::Regex.jira_issue_key_regex).uniq + end + end +end diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb index 33658ae225f..c2266f0bad6 100644 --- a/lib/backup/artifacts.rb +++ b/lib/backup/artifacts.rb @@ -9,7 +9,7 @@ module Backup def initialize(progress) @progress = progress - super('artifacts', JobArtifactUploader.root) + super('artifacts', JobArtifactUploader.root, excludes: ['tmp']) end end end diff --git a/lib/backup/database.rb b/lib/backup/database.rb index d4c1ce260e4..851445f703d 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -8,10 +8,18 @@ module Backup attr_reader :progress attr_reader :config, :db_file_name - def initialize(progress) + IGNORED_ERRORS = [ + # Ignore the DROP errors; recent database dumps will use --if-exists with pg_dump + /does not exist$/, + # User may not have permissions to drop extensions or schemas + /must be owner of/ + ].freeze + IGNORED_ERRORS_REGEXP = Regexp.union(IGNORED_ERRORS).freeze + + def initialize(progress, filename: nil) @progress = progress @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env] - @db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz') + @db_file_name = filename || File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz') end def dump @@ -27,6 +35,7 @@ module Backup progress.print "Dumping PostgreSQL database #{config['database']} ... " pg_env pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump. + pgsql_args << '--if-exists' if Gitlab.config.backup.pg_schema pgsql_args << '-n' @@ -48,6 +57,8 @@ module Backup end report_success(success) + progress.flush + raise Backup::Error, 'Backup failed' unless success end @@ -56,26 +67,65 @@ module Backup decompress_pid = spawn(*%w(gzip -cd), out: decompress_wr, in: db_file_name) decompress_wr.close - restore_pid = + status, errors = case config["adapter"] when "postgresql" then progress.print "Restoring PostgreSQL database #{config['database']} ... " pg_env - spawn('psql', config['database'], in: decompress_rd) + execute_and_track_errors(pg_restore_cmd, decompress_rd) end decompress_rd.close - success = [decompress_pid, restore_pid].all? do |pid| - Process.waitpid(pid) - $?.success? + Process.waitpid(decompress_pid) + success = $?.success? && status.success? + + if errors.present? + progress.print "------ BEGIN ERRORS -----\n".color(:yellow) + progress.print errors.join.color(:yellow) + progress.print "------ END ERRORS -------\n".color(:yellow) end report_success(success) - abort Backup::Error, 'Restore failed' unless success + raise Backup::Error, 'Restore failed' unless success + + errors end protected + def ignore_error?(line) + IGNORED_ERRORS_REGEXP.match?(line) + end + + def execute_and_track_errors(cmd, decompress_rd) + errors = [] + + Open3.popen3(ENV, *cmd) do |stdin, stdout, stderr, thread| + stdin.binmode + + out_reader = Thread.new do + data = stdout.read + $stdout.write(data) + end + + err_reader = Thread.new do + until (raw_line = stderr.gets).nil? + warn(raw_line) + errors << raw_line unless ignore_error?(raw_line) + end + end + + begin + IO.copy_stream(decompress_rd, stdin) + rescue Errno::EPIPE + end + + stdin.close + [thread, out_reader, err_reader].each(&:join) + [thread.value, errors] + end + end + def pg_env args = { 'username' => 'PGUSER', @@ -100,5 +150,11 @@ module Backup progress.puts '[FAILED]'.color(:red) end end + + private + + def pg_restore_cmd + ['psql', config['database']] + end end end diff --git a/lib/backup/files.rb b/lib/backup/files.rb index dae9056a47b..619a62fd6f6 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -7,14 +7,17 @@ module Backup class Files include Backup::Helper - attr_reader :name, :app_files_dir, :backup_tarball, :files_parent_dir + DEFAULT_EXCLUDE = 'lost+found' - def initialize(name, app_files_dir) + attr_reader :name, :app_files_dir, :backup_tarball, :excludes, :files_parent_dir + + def initialize(name, app_files_dir, excludes: []) @name = name @app_files_dir = File.realpath(app_files_dir) @files_parent_dir = File.realpath(File.join(@app_files_dir, '..')) @backup_files_dir = File.join(Gitlab.config.backup.path, File.basename(@app_files_dir) ) @backup_tarball = File.join(Gitlab.config.backup.path, name + '.tar.gz') + @excludes = [DEFAULT_EXCLUDE].concat(excludes) end # Copy files from public/files to backup/files @@ -23,7 +26,7 @@ module Backup FileUtils.rm_f(backup_tarball) if ENV['STRATEGY'] == 'copy' - cmd = %W(rsync -a --exclude=lost+found #{app_files_dir} #{Gitlab.config.backup.path}) + cmd = [%w(rsync -a), exclude_dirs(:rsync), %W(#{app_files_dir} #{Gitlab.config.backup.path})].flatten output, status = Gitlab::Popen.popen(cmd) unless status == 0 @@ -31,10 +34,12 @@ module Backup raise Backup::Error, 'Backup failed' end - run_pipeline!([%W(#{tar} --exclude=lost+found -C #{@backup_files_dir} -cf - .), gzip_cmd], out: [backup_tarball, 'w', 0600]) + tar_cmd = [tar, exclude_dirs(:tar), %W(-C #{@backup_files_dir} -cf - .)].flatten + run_pipeline!([tar_cmd, gzip_cmd], out: [backup_tarball, 'w', 0600]) FileUtils.rm_rf(@backup_files_dir) else - run_pipeline!([%W(#{tar} --exclude=lost+found -C #{app_files_dir} -cf - .), gzip_cmd], out: [backup_tarball, 'w', 0600]) + tar_cmd = [tar, exclude_dirs(:tar), %W(-C #{app_files_dir} -cf - .)].flatten + run_pipeline!([tar_cmd, gzip_cmd], out: [backup_tarball, 'w', 0600]) end end @@ -81,5 +86,17 @@ module Backup error = err_r.read raise Backup::Error, "Backup failed. #{error}" unless error =~ regex end + + def exclude_dirs(fmt) + excludes.map do |s| + if s == DEFAULT_EXCLUDE + '--exclude=' + s + elsif fmt == :rsync + '--exclude=/' + s + elsif fmt == :tar + '--exclude=./' + s + end + end + end end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 915567f8106..2b28b30fd74 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -47,7 +47,7 @@ module Backup return end - directory = connect_to_remote_directory(connection_settings) + directory = connect_to_remote_directory(Gitlab.config.backup.upload) if directory.files.create(create_attributes) progress.puts "done".color(:green) @@ -88,7 +88,7 @@ module Backup # - 1495527097_2017_05_23_9.3.0-pre_gitlab_backup.tar next unless file =~ /^(\d{10})(?:_\d{4}_\d{2}_\d{2}(_\d+\.\d+\.\d+((-|\.)(pre|rc\d))?(-ee)?)?)?_gitlab_backup\.tar$/ - timestamp = $1.to_i + timestamp = Regexp.last_match(1).to_i if Time.at(timestamp) < (Time.now - keep_time) begin @@ -195,9 +195,11 @@ module Backup @backup_file_list.map {|item| item.gsub("#{FILE_NAME_SUFFIX}", "")} end - def connect_to_remote_directory(connection_settings) - # our settings use string keys, but Fog expects symbols - connection = ::Fog::Storage.new(connection_settings.symbolize_keys) + def connect_to_remote_directory(options) + config = ObjectStorage::Config.new(options) + config.load_provider + + connection = ::Fog::Storage.new(config.credentials) # We only attempt to create the directory for local backups. For AWS # and other cloud providers, we cannot guarantee the user will have diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb index a4be728df08..d7aab33d7cb 100644 --- a/lib/backup/pages.rb +++ b/lib/backup/pages.rb @@ -9,7 +9,7 @@ module Backup def initialize(progress) @progress = progress - super('pages', Gitlab.config.pages.path) + super('pages', Gitlab.config.pages.path, excludes: [::Projects::UpdatePagesService::TMP_EXTRACT_PATH]) end end end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 51fac9e8706..eb0b230904e 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -148,20 +148,22 @@ module Backup private def dump_consecutive - Project.find_each(batch_size: 1000) do |project| + Project.includes(:route, :group, namespace: :owner).find_each(batch_size: 1000) do |project| dump_project(project) end end def dump_storage(storage, semaphore, max_storage_concurrency:) errors = Queue.new - queue = SizedQueue.new(1) + queue = InterlockSizedQueue.new(1) threads = Array.new(max_storage_concurrency) do Thread.new do Rails.application.executor.wrap do while project = queue.pop - semaphore.acquire + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + semaphore.acquire + end begin dump_project(project) @@ -176,7 +178,7 @@ module Backup end end - Project.for_repository_storage(storage).find_each(batch_size: 100) do |project| + Project.for_repository_storage(storage).includes(:route, :group, namespace: :owner).find_each(batch_size: 100) do |project| break unless errors.empty? queue.push(project) @@ -241,5 +243,23 @@ module Backup pool.schedule end end + + class InterlockSizedQueue < SizedQueue + extend ::Gitlab::Utils::Override + + override :pop + def pop(*) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + super + end + end + + override :push + def push(*) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + super + end + end + end end end diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index 5a20b6ae0a6..b6a62bc3f29 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -9,7 +9,7 @@ module Backup def initialize(progress) @progress = progress - super('uploads', File.join(Gitlab.config.uploads.storage_path, "uploads")) + super('uploads', File.join(Gitlab.config.uploads.storage_path, "uploads"), excludes: ['tmp']) end end end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index b0a2f6f69d5..2448c2c2bb2 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -265,7 +265,7 @@ module Banzai extras = [] if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/ - extras << "comment #{$1}" + extras << "comment #{Regexp.last_match(1)}" end extension = matches[:extension] if matches.names.include?("extension") @@ -436,7 +436,7 @@ module Banzai escaped = escape_html_entities(text) escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match| - placeholder_data[$1.to_i] + placeholder_data[Regexp.last_match(1).to_i] end end end diff --git a/lib/banzai/filter/alert_reference_filter.rb b/lib/banzai/filter/alert_reference_filter.rb new file mode 100644 index 00000000000..228a4159c99 --- /dev/null +++ b/lib/banzai/filter/alert_reference_filter.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Banzai + module Filter + class AlertReferenceFilter < IssuableReferenceFilter + self.reference_type = :alert + + def self.object_class + AlertManagement::Alert + end + + def self.object_sym + :alert + end + + def parent_records(parent, ids) + parent.alert_management_alerts.where(iid: ids.to_a) + end + + def url_for_object(alert, project) + ::Gitlab::Routing.url_helpers.details_project_alert_management_url( + project, + alert.iid, + only_path: context[:only_path] + ) + end + end + end +end diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index 4723bfbf261..0aa1ee8f604 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -86,7 +86,7 @@ module Banzai # outside the link element. The entity must be marked HTML safe in # order to be output literally rather than escaped. match.gsub!(/((?:&[\w#]+;)+)\z/, '') - dropped = ($1 || '').html_safe + dropped = (Regexp.last_match(1) || '').html_safe # To match the behaviour of Rinku, if the matched link ends with a # closing part of a matched pair of punctuation, we remove that trailing diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb index fa1690f73ad..b32fe5e8301 100644 --- a/lib/banzai/filter/emoji_filter.rb +++ b/lib/banzai/filter/emoji_filter.rb @@ -33,7 +33,7 @@ module Banzai # Returns a String with :emoji: replaced with gl-emoji unicode. def emoji_name_element_unicode_filter(text) text.gsub(emoji_pattern) do |match| - name = $1 + name = Regexp.last_match(1) Gitlab::Emoji.gl_emoji_tag(name) end end diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb index 7928272a2cf..e16de13725f 100644 --- a/lib/banzai/filter/gollum_tags_filter.rb +++ b/lib/banzai/filter/gollum_tags_filter.rb @@ -64,7 +64,7 @@ module Banzai next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) next unless node.content =~ TAGS_PATTERN - html = process_tag($1) + html = process_tag(Regexp.last_match(1)) node.replace(html) if html && html != node.content end diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb index 543d98e62be..2872ad7b632 100644 --- a/lib/banzai/filter/inline_metrics_filter.rb +++ b/lib/banzai/filter/inline_metrics_filter.rb @@ -10,7 +10,6 @@ module Banzai # the cost of doing a full regex match. def xpath_search "descendant-or-self::a[contains(@href,'metrics') and \ - contains(@href,'environments') and \ starts-with(@href, '#{gitlab_domain}')]" end @@ -29,7 +28,7 @@ module Banzai params['project'], params['environment'], embedded: true, - **query_params(params['url']) + **query_params(params['url']).except(:environment) ) end end diff --git a/lib/banzai/pipeline/broadcast_message_pipeline.rb b/lib/banzai/pipeline/broadcast_message_pipeline.rb index e31795e673c..27118269bd0 100644 --- a/lib/banzai/pipeline/broadcast_message_pipeline.rb +++ b/lib/banzai/pipeline/broadcast_message_pipeline.rb @@ -7,7 +7,6 @@ module Banzai @filters ||= FilterArray[ Filter::MarkdownFilter, Filter::BroadcastMessageSanitizationFilter, - Filter::EmojiFilter, Filter::ColorFilter, Filter::AutolinkFilter, diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 10ac813ea15..7057ac9d707 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -12,14 +12,11 @@ module Banzai def self.filters @filters ||= FilterArray[ Filter::PlantumlFilter, - # Must always be before the SanitizationFilter to prevent XSS attacks Filter::SpacedLinkFilter, - Filter::SanitizationFilter, Filter::AssetProxyFilter, Filter::SyntaxHighlightFilter, - Filter::MathFilter, Filter::ColorFilter, Filter::MermaidFilter, @@ -34,13 +31,10 @@ module Banzai Filter::ExternalLinkFilter, Filter::SuggestionFilter, Filter::FootnoteFilter, - *reference_filters, - Filter::EmojiFilter, Filter::TaskListFilter, Filter::InlineDiffFilter, - Filter::SetDirectionFilter ] end @@ -65,7 +59,8 @@ module Banzai Filter::CommitRangeReferenceFilter, Filter::CommitReferenceFilter, Filter::LabelReferenceFilter, - Filter::MilestoneReferenceFilter + Filter::MilestoneReferenceFilter, + Filter::AlertReferenceFilter ] end diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb index 7fe13100ec2..a2fe6d52a90 100644 --- a/lib/banzai/pipeline/single_line_pipeline.rb +++ b/lib/banzai/pipeline/single_line_pipeline.rb @@ -8,11 +8,9 @@ module Banzai Filter::HtmlEntityFilter, Filter::SanitizationFilter, Filter::AssetProxyFilter, - Filter::EmojiFilter, Filter::AutolinkFilter, Filter::ExternalLinkFilter, - *reference_filters ] end @@ -25,7 +23,8 @@ module Banzai Filter::MergeRequestReferenceFilter, Filter::SnippetReferenceFilter, Filter::CommitRangeReferenceFilter, - Filter::CommitReferenceFilter + Filter::CommitReferenceFilter, + Filter::AlertReferenceFilter ] end diff --git a/lib/banzai/reference_parser/alert_parser.rb b/lib/banzai/reference_parser/alert_parser.rb new file mode 100644 index 00000000000..7b864d26f67 --- /dev/null +++ b/lib/banzai/reference_parser/alert_parser.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Banzai + module ReferenceParser + class AlertParser < BaseParser + self.reference_type = :alert + + def references_relation + AlertManagement::Alert + end + + private + + def can_read_reference?(user, alert, node) + can?(user, :read_alert_management_alert, alert) + end + end + end +end diff --git a/lib/bitbucket_server/representation/comment.rb b/lib/bitbucket_server/representation/comment.rb index 99b97a3b181..7e55c446e76 100644 --- a/lib/bitbucket_server/representation/comment.rb +++ b/lib/bitbucket_server/representation/comment.rb @@ -38,7 +38,9 @@ module BitbucketServer end def author_username - author['displayName'] + author['username'] || + author['slug'] || + author['displayName'] end def author_email diff --git a/lib/bitbucket_server/representation/pull_request.rb b/lib/bitbucket_server/representation/pull_request.rb index c3e927d8de7..2f377bdced2 100644 --- a/lib/bitbucket_server/representation/pull_request.rb +++ b/lib/bitbucket_server/representation/pull_request.rb @@ -11,6 +11,12 @@ module BitbucketServer raw.dig('author', 'user', 'emailAddress') end + def author_username + raw.dig('author', 'user', 'username') || + raw.dig('author', 'user', 'slug') || + raw.dig('author', 'user', 'displayName') + end + def description raw['description'] end diff --git a/lib/carrier_wave_string_file.rb b/lib/carrier_wave_string_file.rb index c9a64d9e631..b6bc3d986ca 100644 --- a/lib/carrier_wave_string_file.rb +++ b/lib/carrier_wave_string_file.rb @@ -4,4 +4,12 @@ class CarrierWaveStringFile < StringIO def original_filename "" end + + def self.new_file(file_content:, filename:, content_type: "application/octet-stream") + { + "tempfile" => StringIO.new(file_content), + "filename" => filename, + "content_type" => content_type + } + end end diff --git a/lib/constraints/jira_encoded_url_constrainer.rb b/lib/constraints/jira_encoded_url_constrainer.rb new file mode 100644 index 00000000000..92e2fff346b --- /dev/null +++ b/lib/constraints/jira_encoded_url_constrainer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Constraints + class JiraEncodedUrlConstrainer + def matches?(request) + request.path.starts_with?('/-/jira') || request.params[:project_id].include?(Gitlab::Jira::Dvcs::ENCODED_SLASH) + end + end +end diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index 118eb8e2d7c..e6ca33d749b 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -21,6 +21,17 @@ module ContainerRegistry # Taken from: FaradayMiddleware::FollowRedirects REDIRECT_CODES = Set.new [301, 302, 303, 307] + def self.supports_tag_delete? + registry_config = Gitlab.config.registry + return false unless registry_config.enabled && registry_config.api_url.present? + + return true if ::Gitlab.com? + + token = Auth::ContainerRegistryAuthenticationService.access_token([], []) + client = new(registry_config.api_url, token: token) + client.supports_tag_delete? + end + def initialize(base_uri, options = {}) @base_uri = base_uri @options = options diff --git a/lib/expand_variables.rb b/lib/expand_variables.rb index 45af30f46dc..3a50925d628 100644 --- a/lib/expand_variables.rb +++ b/lib/expand_variables.rb @@ -7,7 +7,7 @@ module ExpandVariables value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do variables_hash ||= transform_variables(variables) - variables_hash[$1 || $2] + variables_hash[Regexp.last_match(1) || Regexp.last_match(2)] end end diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb index 5ef2d888550..adbe93cfa3a 100644 --- a/lib/extracts_ref.rb +++ b/lib/extracts_ref.rb @@ -111,7 +111,6 @@ module ExtractsRef end def use_first_path_segment?(ref) - return false unless ::Feature.enabled?(:extracts_path_optimization) return false unless repository_container return false if repository_container.repository.has_ambiguous_refs? diff --git a/lib/feature.rb b/lib/feature.rb index 7cf40b63fdf..71241e98723 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -18,6 +18,10 @@ class Feature superclass.table_name = 'feature_gates' end + class ActiveSupportCacheStoreAdapter < Flipper::Adapters::ActiveSupportCacheStore + # overrides methods in EE + end + InvalidFeatureFlagError = Class.new(Exception) # rubocop:disable Lint/InheritException class << self @@ -137,6 +141,12 @@ class Feature Feature::Definition.load_all! end + def register_hot_reloader + return unless check_feature_flags_definition? + + Feature::Definition.register_hot_reloader! + end + private def flipper @@ -154,7 +164,7 @@ class Feature # Redis L2 cache redis_cache_adapter = - Flipper::Adapters::ActiveSupportCacheStore.new( + ActiveSupportCacheStoreAdapter.new( active_record_adapter, l2_cache_backend, expires_in: 1.hour) @@ -231,4 +241,4 @@ class Feature end end -Feature.prepend_if_ee('EE::Feature') +Feature::ActiveSupportCacheStoreAdapter.prepend_if_ee('EE::Feature::ActiveSupportCacheStoreAdapter') diff --git a/lib/feature/definition.rb b/lib/feature/definition.rb index b0ea55c5805..ee779a86952 100644 --- a/lib/feature/definition.rb +++ b/lib/feature/definition.rb @@ -107,6 +107,20 @@ class Feature 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 + # We use `Feature::Definition` as on Ruby code-reload + # a new class definition is created + Feature::Definition.load_all! + end + + Rails.application.reloaders << file_watcher + Rails.application.reloader.to_run { file_watcher.execute_if_updated } + + file_watcher + end + private def load_from_file(path) @@ -130,6 +144,19 @@ class Feature definitions[definition.key] = definition end end + + def reload_files + [File.expand_path(__FILE__)] + end + + def reload_directories + paths.each_with_object({}) do |path, result| + path = File.dirname(path) + Dir.glob(path).each do |matching_dir| + result[matching_dir] = 'yml' + end + end + end end end end diff --git a/lib/feature/shared.rb b/lib/feature/shared.rb index 14efbb07100..c06f699ef27 100644 --- a/lib/feature/shared.rb +++ b/lib/feature/shared.rb @@ -8,15 +8,38 @@ class Feature module Shared # optional: defines if a on-disk definition is required for this feature flag type # rollout_issue: defines if `bin/feature-flag` asks for rollout issue + # default_enabled: defines a default state of a feature flag when created by `bin/feature-flag` # example: usage being shown when exception is raised TYPES = { development: { description: 'Short lived, used to enable unfinished code to be deployed', optional: true, rollout_issue: true, + default_enabled: false, example: <<-EOS - Feature.enabled?(:my_feature_flag) - Feature.enabled?(:my_feature_flag, type: :development) + Feature.enabled?(:my_feature_flag, project) + Feature.enabled?(:my_feature_flag, project, type: :development) + push_frontend_feature_flag?(:my_feature_flag, project) + EOS + }, + ops: { + description: "Long-lived feature flags that control operational aspects of GitLab's behavior", + optional: true, + rollout_issue: false, + default_enabled: false, + example: <<-EOS + Feature.enabled?(:my_ops_flag, type: ops) + push_frontend_feature_flag?(:my_ops_flag, project, type: :ops) + EOS + }, + licensed: { + description: 'Permanent feature flags used to temporarily disable licensed features.', + optional: true, + rollout_issue: false, + default_enabled: true, + example: <<-EOS + project.feature_available?(:my_licensed_feature) + namespace.feature_available?(:my_licensed_feature) EOS } }.freeze diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index bf4438fb518..830980f0997 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -9,12 +9,13 @@ module Gitlab module Access AccessDeniedError = Class.new(StandardError) - NO_ACCESS = 0 - GUEST = 10 - REPORTER = 20 - DEVELOPER = 30 - MAINTAINER = 40 - OWNER = 50 + NO_ACCESS = 0 + MINIMAL_ACCESS = 5 + GUEST = 10 + REPORTER = 20 + DEVELOPER = 30 + MAINTAINER = 40 + OWNER = 50 # Branch protection settings PROTECTION_NONE = 0 diff --git a/lib/gitlab/alert_management/alert_params.rb b/lib/gitlab/alert_management/alert_params.rb index 84a75e62ecf..3bb839c1114 100644 --- a/lib/gitlab/alert_management/alert_params.rb +++ b/lib/gitlab/alert_management/alert_params.rb @@ -20,8 +20,10 @@ module Gitlab hosts: Array(annotations[:hosts]), payload: payload, started_at: parsed_payload['startsAt'], + ended_at: parsed_payload['endsAt'], severity: annotations[:severity], - fingerprint: annotations[:fingerprint] + fingerprint: annotations[:fingerprint], + environment: annotations[:environment] } end diff --git a/lib/gitlab/alert_management/payload.rb b/lib/gitlab/alert_management/payload.rb new file mode 100644 index 00000000000..177d544d720 --- /dev/null +++ b/lib/gitlab/alert_management/payload.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Gitlab + module AlertManagement + module Payload + MONITORING_TOOLS = { + prometheus: 'Prometheus' + }.freeze + + class << self + # Instantiates an instance of a subclass of + # Gitlab::AlertManagement::Payload::Base. This can + # be used to create new alerts or read content from + # the payload of an existing AlertManagement::Alert + # + # @param project [Project] + # @param payload [Hash] + # @param monitoring_tool [String] + def parse(project, payload, monitoring_tool: nil) + payload_class = payload_class_for( + monitoring_tool: monitoring_tool || payload&.dig('monitoring_tool'), + payload: payload + ) + + payload_class.new(project: project, payload: payload) + end + + private + + def payload_class_for(monitoring_tool:, payload:) + if monitoring_tool == MONITORING_TOOLS[:prometheus] + if gitlab_managed_prometheus?(payload) + ::Gitlab::AlertManagement::Payload::ManagedPrometheus + else + ::Gitlab::AlertManagement::Payload::Prometheus + end + else + ::Gitlab::AlertManagement::Payload::Generic + end + end + + def gitlab_managed_prometheus?(payload) + payload&.dig('labels', 'gitlab_alert_id').present? + end + end + end + end +end diff --git a/lib/gitlab/alert_management/payload/base.rb b/lib/gitlab/alert_management/payload/base.rb new file mode 100644 index 00000000000..74e47e5226e --- /dev/null +++ b/lib/gitlab/alert_management/payload/base.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +# Representation of a payload of an alert. Defines a constant +# API so that payloads from various sources can be treated +# identically. Subclasses should define how to parse payload +# based on source of alert. +module Gitlab + module AlertManagement + module Payload + class Base + include ActiveModel::Model + include Gitlab::Utils::StrongMemoize + include Gitlab::Routing + + attr_accessor :project, :payload + + # Any attribute expected to be specifically read from + # or derived from an alert payload should be defined. + EXPECTED_PAYLOAD_ATTRIBUTES = [ + :alert_markdown, + :alert_title, + :annotations, + :description, + :ends_at, + :environment, + :environment_name, + :full_query, + :generator_url, + :gitlab_alert, + :gitlab_fingerprint, + :gitlab_prometheus_alert_id, + :gitlab_y_label, + :has_required_attributes?, + :hosts, + :metric_id, + :metrics_dashboard_url, + :monitoring_tool, + :resolved?, + :runbook, + :service, + :severity, + :starts_at, + :status, + :title + ].freeze + + # Define expected API for a payload + EXPECTED_PAYLOAD_ATTRIBUTES.each do |key| + define_method(key) {} + end + + # Defines a method which allows access to a given + # value within an alert payload + # + # @param key [Symbol] Name expected to be used to reference value + # @param paths [String, Array<String>, Array<Array<String>>,] + # List of (nested) keys at value can be found, the + # first to yield a result will be used + # @param type [Symbol] If value should be converted to another type, + # that should be specified here + # @param fallback [Proc] Block to be executed to yield a value if + # a value cannot be idenitied at any provided paths + # Example) + # attribute :title + # paths: [['title'], + # ['details', 'title']] + # fallback: Proc.new { 'New Alert' } + # + # The above sample definition will define a method + # called #title which will return the value from the + # payload under the key `title` if available, otherwise + # looking under `details.title`. If neither returns a + # value, the return value will be `'New Alert'` + def self.attribute(key, paths:, type: nil, fallback: -> { nil }) + define_method(key) do + strong_memoize(key) do + paths = Array(paths).first.is_a?(String) ? [Array(paths)] : paths + value = value_for_paths(paths) + value = parse_value(value, type) if value + + value.presence || fallback.call + end + end + end + + # Attributes of an AlertManagement::Alert as read + # directly from a payload. Prefer accessing + # AlertManagement::Alert directly for read operations. + def alert_params + { + description: description, + ended_at: ends_at, + environment: environment, + fingerprint: gitlab_fingerprint, + hosts: Array(hosts), + monitoring_tool: monitoring_tool, + payload: payload, + project_id: project.id, + prometheus_alert: gitlab_alert, + service: service, + severity: severity, + started_at: starts_at, + title: title + }.transform_values(&:presence).compact + end + + def gitlab_fingerprint + strong_memoize(:gitlab_fingerprint) do + next unless plain_gitlab_fingerprint + + Gitlab::AlertManagement::Fingerprint.generate(plain_gitlab_fingerprint) + end + end + + def environment + strong_memoize(:environment) do + next unless environment_name + + EnvironmentsFinder + .new(project, nil, { name: environment_name }) + .find + .first + end + end + + def resolved? + status == 'resolved' + end + + def has_required_attributes? + true + end + + private + + def plain_gitlab_fingerprint; end + + def value_for_paths(paths) + target_path = paths.find { |path| payload&.dig(*path) } + + payload&.dig(*target_path) if target_path + end + + def parse_value(value, type) + case type + when :time + parse_time(value) + when :integer + parse_integer(value) + else + value + end + end + + def parse_time(value) + Time.parse(value).utc + rescue ArgumentError + end + + def parse_integer(value) + Integer(value) + rescue ArgumentError, TypeError + end + end + end + end +end diff --git a/lib/gitlab/alert_management/payload/generic.rb b/lib/gitlab/alert_management/payload/generic.rb new file mode 100644 index 00000000000..7efdfac75dc --- /dev/null +++ b/lib/gitlab/alert_management/payload/generic.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Attribute mapping for alerts via generic alerting integration. +module Gitlab + module AlertManagement + module Payload + class Generic < Base + DEFAULT_TITLE = 'New: Incident' + DEFAULT_SEVERITY = 'critical' + + attribute :environment_name, paths: 'gitlab_environment_name' + attribute :hosts, paths: 'hosts' + attribute :monitoring_tool, paths: 'monitoring_tool' + attribute :runbook, paths: 'runbook' + attribute :service, paths: 'service' + attribute :severity, paths: 'severity', fallback: -> { DEFAULT_SEVERITY } + attribute :starts_at, paths: 'start_time', type: :time, fallback: -> { Time.current.utc } + attribute :title, paths: 'title', fallback: -> { DEFAULT_TITLE } + + attribute :plain_gitlab_fingerprint, paths: 'fingerprint' + private :plain_gitlab_fingerprint + end + end + end +end diff --git a/lib/gitlab/alert_management/payload/managed_prometheus.rb b/lib/gitlab/alert_management/payload/managed_prometheus.rb new file mode 100644 index 00000000000..2236e60a0c6 --- /dev/null +++ b/lib/gitlab/alert_management/payload/managed_prometheus.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# Attribute mapping for alerts via prometheus alerting integration, +# and for which payload includes gitlab-controlled attributes. +module Gitlab + module AlertManagement + module Payload + class ManagedPrometheus < ::Gitlab::AlertManagement::Payload::Prometheus + attribute :gitlab_prometheus_alert_id, + paths: %w(labels gitlab_prometheus_alert_id), + type: :integer + attribute :metric_id, + paths: %w(labels gitlab_alert_id), + type: :integer + + def gitlab_alert + strong_memoize(:gitlab_alert) do + next unless metric_id || gitlab_prometheus_alert_id + + alerts = Projects::Prometheus::AlertsFinder + .new(project: project, metric: metric_id, id: gitlab_prometheus_alert_id) + .execute + + next if alerts.blank? || alerts.size > 1 + + alerts.first + end + end + + def full_query + gitlab_alert&.full_query || super + end + + def environment + gitlab_alert&.environment || super + end + + def metrics_dashboard_url + return unless gitlab_alert + + metrics_dashboard_project_prometheus_alert_url( + project, + gitlab_alert.prometheus_metric_id, + environment_id: environment.id, + embedded: true, + **alert_embed_window_params + ) + end + + private + + def plain_gitlab_fingerprint + [metric_id, starts_at_raw].join('/') + end + end + end + end +end diff --git a/lib/gitlab/alert_management/payload/prometheus.rb b/lib/gitlab/alert_management/payload/prometheus.rb new file mode 100644 index 00000000000..336e9b319e8 --- /dev/null +++ b/lib/gitlab/alert_management/payload/prometheus.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +# Attribute mapping for alerts via prometheus alerting integration. +module Gitlab + module AlertManagement + module Payload + class Prometheus < Base + attribute :alert_markdown, paths: %w(annotations gitlab_incident_markdown) + attribute :annotations, paths: 'annotations' + attribute :description, paths: %w(annotations description) + attribute :ends_at, paths: 'endsAt', type: :time + attribute :environment_name, paths: %w(labels gitlab_environment_name) + attribute :generator_url, paths: %w(generatorURL) + attribute :gitlab_y_label, + paths: [%w(annotations gitlab_y_label), + %w(annotations title), + %w(annotations summary), + %w(labels alertname)] + attribute :runbook, paths: %w(annotations runbook) + attribute :starts_at, + paths: 'startsAt', + type: :time, + fallback: -> { Time.current.utc } + attribute :status, paths: 'status' + attribute :title, + paths: [%w(annotations title), + %w(annotations summary), + %w(labels alertname)] + + attribute :starts_at_raw, + paths: [%w(startsAt)] + private :starts_at_raw + + METRIC_TIME_WINDOW = 30.minutes + + def monitoring_tool + Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus] + end + + # Parses `g0.expr` from `generatorURL`. + # + # Example: http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1 + def full_query + return unless generator_url + + uri = URI(generator_url) + + Rack::Utils.parse_query(uri.query).fetch('g0.expr') + rescue URI::InvalidURIError, KeyError + end + + def metrics_dashboard_url + return unless environment && full_query && title + + metrics_dashboard_project_environment_url( + project, + environment, + embed_json: dashboard_json, + embedded: true, + **alert_embed_window_params + ) + end + + def has_required_attributes? + project && title && starts_at_raw + end + + private + + def plain_gitlab_fingerprint + [starts_at_raw, title, full_query].join('/') + end + + # Formatted for parsing by JS + def alert_embed_window_params + { + start: (starts_at - METRIC_TIME_WINDOW).utc.strftime('%FT%TZ'), + end: (starts_at + METRIC_TIME_WINDOW).utc.strftime('%FT%TZ') + } + end + + def dashboard_json + { + panel_groups: [{ + panels: [{ + type: 'area-chart', + title: title, + y_label: gitlab_y_label, + metrics: [{ + query_range: full_query + }] + }] + }] + }.to_json + end + end + end + end +end diff --git a/lib/gitlab/alerting/notification_payload_parser.rb b/lib/gitlab/alerting/notification_payload_parser.rb index f285dcf507f..348f851f551 100644 --- a/lib/gitlab/alerting/notification_payload_parser.rb +++ b/lib/gitlab/alerting/notification_payload_parser.rb @@ -20,7 +20,8 @@ module Gitlab def call { 'annotations' => annotations, - 'startsAt' => starts_at + 'startsAt' => starts_at, + 'endsAt' => ends_at }.compact end @@ -55,7 +56,8 @@ module Gitlab 'service' => payload[:service], 'hosts' => hosts.presence, 'severity' => severity, - 'fingerprint' => fingerprint + 'fingerprint' => fingerprint, + 'environment' => environment } end @@ -73,8 +75,24 @@ module Gitlab current_time end + def ends_at + Time.parse(payload[:end_time].to_s).rfc3339 + rescue ArgumentError + nil + end + + def environment + environment_name = payload[:gitlab_environment_name] + + return unless environment_name + + EnvironmentsFinder.new(project, nil, { name: environment_name }) + .find + &.first + end + def secondary_params - payload.except(:start_time) + payload.except(:start_time, :end_time) end def flatten_secondary_params diff --git a/lib/gitlab/analytics/cycle_analytics/default_stages.rb b/lib/gitlab/analytics/cycle_analytics/default_stages.rb index 79e60e28fc7..fc91dd6e138 100644 --- a/lib/gitlab/analytics/cycle_analytics/default_stages.rb +++ b/lib/gitlab/analytics/cycle_analytics/default_stages.rb @@ -18,8 +18,7 @@ module Gitlab params_for_code_stage, params_for_test_stage, params_for_review_stage, - params_for_staging_stage, - params_for_production_stage + params_for_staging_stage ] end @@ -86,16 +85,6 @@ module Gitlab end_event_identifier: :merge_request_first_deployed_to_production } end - - def self.params_for_production_stage - { - name: 'production', - custom: false, - relative_position: 7, - start_event_identifier: :issue_created, - end_event_identifier: :production_stage_end - } - end end end end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb index cf05ebeb706..b778364a917 100644 --- a/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb @@ -6,7 +6,7 @@ module Gitlab module StageEvents class ProductionStageEnd < StageEvent def self.name - _("Issue first depoloyed to production") + _("Issue first deployed to production") end def self.identifier diff --git a/lib/gitlab/analytics/instance_statistics/workers_argument_builder.rb b/lib/gitlab/analytics/instance_statistics/workers_argument_builder.rb new file mode 100644 index 00000000000..636bba22c23 --- /dev/null +++ b/lib/gitlab/analytics/instance_statistics/workers_argument_builder.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module InstanceStatistics + class WorkersArgumentBuilder + def initialize(measurement_identifiers: [], recorded_at: Time.zone.now) + @measurement_identifiers = measurement_identifiers + @recorded_at = recorded_at + end + + def execute + measurement_identifiers.map do |measurement_identifier| + query_scope = ::Analytics::InstanceStatistics::Measurement::IDENTIFIER_QUERY_MAPPING[measurement_identifier]&.call + + next if query_scope.nil? + + # Determining the query range (id range) as early as possible in order to get more accurate counts. + start = query_scope.minimum(:id) + finish = query_scope.maximum(:id) + + [measurement_identifier, start, finish, recorded_at] + end.compact + end + + private + + attr_reader :measurement_identifiers, :recorded_at + end + end + end +end diff --git a/lib/gitlab/analytics/unique_visits.rb b/lib/gitlab/analytics/unique_visits.rb index ad746ebbd42..292048dcad9 100644 --- a/lib/gitlab/analytics/unique_visits.rb +++ b/lib/gitlab/analytics/unique_visits.rb @@ -14,23 +14,23 @@ module Gitlab # @param [ActiveSupport::TimeWithZone] end_date end of time frame # @return [Integer] number of unique visitors def unique_visits_for(targets:, start_date: 7.days.ago, end_date: start_date + 1.week) - target_ids = if targets == :analytics - self.class.analytics_ids - elsif targets == :compliance - self.class.compliance_ids - else - Array(targets) - end + events = if targets == :analytics + self.class.analytics_events + elsif targets == :compliance + self.class.compliance_events + else + Array(targets) + end - Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: target_ids, start_date: start_date, end_date: end_date) + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, start_date: start_date, end_date: end_date) end class << self - def analytics_ids + def analytics_events Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('analytics') end - def compliance_ids + def compliance_events Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('compliance') end end diff --git a/lib/gitlab/anonymous_session.rb b/lib/gitlab/anonymous_session.rb index 148b6d3310d..911825eef3a 100644 --- a/lib/gitlab/anonymous_session.rb +++ b/lib/gitlab/anonymous_session.rb @@ -2,35 +2,34 @@ module Gitlab class AnonymousSession - def initialize(remote_ip, session_id: nil) + def initialize(remote_ip) @remote_ip = remote_ip - @session_id = session_id end - def store_session_id_per_ip + def count_session_ip Gitlab::Redis::SharedState.with do |redis| redis.pipelined do - redis.sadd(session_lookup_name, session_id) + redis.incr(session_lookup_name) redis.expire(session_lookup_name, 24.hours) end end end - def stored_sessions + def session_count Gitlab::Redis::SharedState.with do |redis| - redis.scard(session_lookup_name) + redis.get(session_lookup_name).to_i end end - def cleanup_session_per_ip_entries + def cleanup_session_per_ip_count Gitlab::Redis::SharedState.with do |redis| - redis.srem(session_lookup_name, session_id) + redis.del(session_lookup_name) end end private - attr_reader :remote_ip, :session_id + attr_reader :remote_ip def session_lookup_name @session_lookup_name ||= "#{Gitlab::Redis::SharedState::IP_SESSIONS_LOOKUP_NAMESPACE}:#{remote_ip}" diff --git a/lib/gitlab/application_context.rb b/lib/gitlab/application_context.rb index a3feda9bb59..30cb74bcf54 100644 --- a/lib/gitlab/application_context.rb +++ b/lib/gitlab/application_context.rb @@ -29,7 +29,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/auth.rb b/lib/gitlab/auth.rb index ece4946383d..609eef5e365 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -50,7 +50,7 @@ module Gitlab build_access_token_check(login, password) || lfs_token_check(login, password, project) || oauth_access_token_check(login, password) || - personal_access_token_check(password) || + personal_access_token_check(password, project) || deploy_token_check(login, password, project) || user_with_password_for_git(login, password) || Gitlab::Auth::Result.new @@ -117,7 +117,6 @@ module Gitlab private - # rubocop:disable Gitlab/RailsLogger def rate_limit!(rate_limiter, success:, login:) return if skip_rate_limit?(login: login) @@ -132,12 +131,11 @@ module Gitlab # This returns true when the failures are over the threshold and the IP # is banned. if rate_limiter.register_fail! - Rails.logger.info "IP #{rate_limiter.ip} failed to login " \ + Gitlab::AppLogger.info "IP #{rate_limiter.ip} failed to login " \ "as #{login} but has been temporarily banned from Git auth" end end end - # rubocop:enable Gitlab/RailsLogger def skip_rate_limit?(login:) CI_JOB_USER == login @@ -191,12 +189,18 @@ module Gitlab end end - def personal_access_token_check(password) + def personal_access_token_check(password, project) return unless password.present? token = PersonalAccessTokensFinder.new(state: 'active').find_by_token(password) - if token && valid_scoped_token?(token, all_available_scopes) && token.user.can?(:log_in) + 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) Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes)) end end diff --git a/lib/gitlab/auth/atlassian/auth_hash.rb b/lib/gitlab/auth/atlassian/auth_hash.rb new file mode 100644 index 00000000000..047e4eabc51 --- /dev/null +++ b/lib/gitlab/auth/atlassian/auth_hash.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Atlassian + class AuthHash < Gitlab::Auth::OAuth::AuthHash + def token + credentials[:token] + end + + def refresh_token + credentials[:refresh_token] + end + + def expires? + credentials[:expires] + end + + def expires_at + credentials[:expires_at] + end + + private + + def credentials + auth_hash[:credentials] + end + end + end + end +end diff --git a/lib/gitlab/auth/atlassian/identity_linker.rb b/lib/gitlab/auth/atlassian/identity_linker.rb new file mode 100644 index 00000000000..4dec54d44d6 --- /dev/null +++ b/lib/gitlab/auth/atlassian/identity_linker.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Atlassian + class IdentityLinker < OmniauthIdentityLinkerBase + extend ::Gitlab::Utils::Override + include ::Gitlab::Utils::StrongMemoize + + private + + override :identity + def identity + strong_memoize(:identity) do + current_user.atlassian_identity || build_atlassian_identity + end + end + + def build_atlassian_identity + identity = current_user.build_atlassian_identity + ::Gitlab::Auth::Atlassian::User.assign_identity_from_auth_hash!(identity, auth_hash) + end + + def auth_hash + ::Gitlab::Auth::Atlassian::AuthHash.new(oauth) + end + end + end + end +end diff --git a/lib/gitlab/auth/atlassian/user.rb b/lib/gitlab/auth/atlassian/user.rb new file mode 100644 index 00000000000..6ab7741cc54 --- /dev/null +++ b/lib/gitlab/auth/atlassian/user.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Atlassian + class User < Gitlab::Auth::OAuth::User + def self.assign_identity_from_auth_hash!(identity, auth_hash) + identity.extern_uid = auth_hash.uid + identity.token = auth_hash.token + identity.refresh_token = auth_hash.refresh_token + identity.expires_at = Time.at(auth_hash.expires_at).utc.to_datetime if auth_hash.expires? + + identity + end + + protected + + def find_by_uid_and_provider + ::Atlassian::Identity.find_by_extern_uid(auth_hash.uid)&.user + end + + def add_or_update_user_identities + return unless gl_user + + identity = gl_user.atlassian_identity || gl_user.build_atlassian_identity + self.class.assign_identity_from_auth_hash!(identity, auth_hash) + end + + def auth_hash=(auth_hash) + @auth_hash = ::Gitlab::Auth::Atlassian::AuthHash.new(auth_hash) + end + end + end + end +end diff --git a/lib/gitlab/auth/ldap/adapter.rb b/lib/gitlab/auth/ldap/adapter.rb index 4f448211abf..b7bb61f0677 100644 --- a/lib/gitlab/auth/ldap/adapter.rb +++ b/lib/gitlab/auth/ldap/adapter.rb @@ -55,7 +55,7 @@ module Gitlab response = ldap.get_operation_result unless response.code == 0 - Rails.logger.warn("LDAP search error: #{response.message}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("LDAP search error: #{response.message}") end [] @@ -67,7 +67,7 @@ module Gitlab retries += 1 error_message = connection_error_message(error) - Rails.logger.warn(error_message) # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn(error_message) if retries < MAX_SEARCH_RETRIES renew_connection_adapter diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb index 7677189eb9f..88cc840c395 100644 --- a/lib/gitlab/auth/ldap/config.rb +++ b/lib/gitlab/auth/ldap/config.rb @@ -248,7 +248,7 @@ module Gitlab begin custom_options[:cert] = OpenSSL::X509::Certificate.new(custom_options[:cert]) rescue OpenSSL::X509::CertificateError => e - Rails.logger.error "LDAP TLS Options 'cert' is invalid for provider #{provider}: #{e.message}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error "LDAP TLS Options 'cert' is invalid for provider #{provider}: #{e.message}" end end @@ -256,7 +256,7 @@ module Gitlab begin custom_options[:key] = OpenSSL::PKey.read(custom_options[:key]) rescue OpenSSL::PKey::PKeyError => e - Rails.logger.error "LDAP TLS Options 'key' is invalid for provider #{provider}: #{e.message}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error "LDAP TLS Options 'key' is invalid for provider #{provider}: #{e.message}" end end diff --git a/lib/gitlab/auth/ldap/person.rb b/lib/gitlab/auth/ldap/person.rb index 8c5000147c4..102820d6bd5 100644 --- a/lib/gitlab/auth/ldap/person.rb +++ b/lib/gitlab/auth/ldap/person.rb @@ -45,7 +45,7 @@ module Gitlab def self.normalize_dn(dn) ::Gitlab::Auth::Ldap::DN.new(dn).to_normalized_s rescue ::Gitlab::Auth::Ldap::DN::FormatError => e - Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}") dn end @@ -57,13 +57,13 @@ module Gitlab def self.normalize_uid(uid) ::Gitlab::Auth::Ldap::DN.normalize_value(uid) rescue ::Gitlab::Auth::Ldap::DN::FormatError => e - Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}") uid end def initialize(entry, provider) - Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" } # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.debug "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" @entry = entry @provider = provider end diff --git a/lib/gitlab/auth/o_auth/provider.rb b/lib/gitlab/auth/o_auth/provider.rb index 1ca59aa827b..1eae7af442d 100644 --- a/lib/gitlab/auth/o_auth/provider.rb +++ b/lib/gitlab/auth/o_auth/provider.rb @@ -5,10 +5,11 @@ module Gitlab module OAuth class Provider LABELS = { - "github" => "GitHub", - "gitlab" => "GitLab.com", - "google_oauth2" => "Google", - "azure_oauth2" => "Azure AD" + "github" => "GitHub", + "gitlab" => "GitLab.com", + "google_oauth2" => "Google", + "azure_oauth2" => "Azure AD", + 'atlassian_oauth2' => 'Atlassian' }.freeze def self.authentication(user, provider) diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb index 086f4a2e91c..3211d2ffaea 100644 --- a/lib/gitlab/auth/o_auth/user.rb +++ b/lib/gitlab/auth/o_auth/user.rb @@ -273,7 +273,11 @@ module Gitlab end def auto_link_user? - Gitlab.config.omniauth.auto_link_user + auto_link = Gitlab.config.omniauth.auto_link_user + return auto_link if [true, false].include?(auto_link) + + auto_link = Array(auto_link) + auto_link.include?(auth_hash.provider) end end end diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb index ea0307e8bd6..d1b9062a23c 100644 --- a/lib/gitlab/background_migration.rb +++ b/lib/gitlab/background_migration.rb @@ -9,7 +9,7 @@ module Gitlab # Begins stealing jobs from the background migrations queue, blocking the # caller until all jobs have been completed. # - # When a migration raises a StandardError is is going to be retries up to + # When a migration raises a StandardError it is going to retry up to # three times, for example, to recover from a deadlock. # # When Exception is being raised, it enqueues the migration again, and diff --git a/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb b/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb index c912628d0fc..5b9ee8a0ee2 100644 --- a/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb +++ b/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb @@ -9,7 +9,7 @@ module Gitlab end def perform(start_id, stop_id) - Rails.logger.info("Setting commits_count for merge request diffs: #{start_id} - #{stop_id}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.info("Setting commits_count for merge request diffs: #{start_id} - #{stop_id}") update = ' commits_count = ( diff --git a/lib/gitlab/background_migration/calculate_wiki_sizes.rb b/lib/gitlab/background_migration/calculate_wiki_sizes.rb index e62f5edd0e7..76598f6e2a6 100644 --- a/lib/gitlab/background_migration/calculate_wiki_sizes.rb +++ b/lib/gitlab/background_migration/calculate_wiki_sizes.rb @@ -10,7 +10,7 @@ module Gitlab .includes(project: [:route, :group, namespace: [:owner]]).find_each do |statistics| statistics.refresh!(only: [:wiki_size]) rescue => e - Rails.logger.error "Failed to update wiki statistics. id: #{statistics.id} message: #{e.message}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error "Failed to update wiki statistics. id: #{statistics.id} message: #{e.message}" end end end diff --git a/lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb b/lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb index 4016b807f21..c0099d44b5a 100644 --- a/lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb +++ b/lib/gitlab/background_migration/fill_valid_time_for_pages_domain_certificate.rb @@ -25,7 +25,7 @@ module Gitlab certificate_valid_not_after: domain.x509&.not_after&.iso8601 ) rescue => e - Rails.logger.error "Failed to update pages domain certificate valid time. id: #{domain.id}, message: #{e.message}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error "Failed to update pages domain certificate valid time. id: #{domain.id}, message: #{e.message}" end end end diff --git a/lib/gitlab/background_migration/fix_pages_access_level.rb b/lib/gitlab/background_migration/fix_pages_access_level.rb index 31d2e78b2d2..8e46021bd93 100644 --- a/lib/gitlab/background_migration/fix_pages_access_level.rb +++ b/lib/gitlab/background_migration/fix_pages_access_level.rb @@ -103,8 +103,8 @@ module Gitlab end # Private projects are not allowed to have enabled access level, only `private` and `public` - # If access control is enabled, these projects currently behave as if the have `private` pages_access_level - # if access control is disabled, these projects currently behave as if the have `public` pages_access_level + # If access control is enabled, these projects currently behave as if they have `private` pages_access_level + # if access control is disabled, these projects currently behave as if they have `public` pages_access_level # so we preserve this behaviour for projects with pages already deployed # for project without pages we always set `private` access_level def fix_private_access_level(start_id, stop_id) diff --git a/lib/gitlab/background_migration/migrate_to_hashed_storage.rb b/lib/gitlab/background_migration/migrate_to_hashed_storage.rb new file mode 100644 index 00000000000..4054db4fb87 --- /dev/null +++ b/lib/gitlab/background_migration/migrate_to_hashed_storage.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # Background migration to move any legacy project to Hashed Storage + class MigrateToHashedStorage + def perform + batch_size = helper.batch_size + legacy_projects_count = Project.with_unmigrated_storage.count + + if storage_migrator.rollback_pending? + logger.warn( + migrator: 'MigrateToHashedStorage', + message: 'Aborting an storage rollback operation currently in progress' + ) + + storage_migrator.abort_rollback! + end + + if legacy_projects_count == 0 + logger.info( + migrator: 'MigrateToHashedStorage', + message: 'There are no projects requiring migration to Hashed Storage' + ) + + return + end + + logger.info( + migrator: 'MigrateToHashedStorage', + message: "Enqueuing migration of #{legacy_projects_count} projects in batches of #{batch_size}" + ) + + helper.project_id_batches_migration do |start, finish| + storage_migrator.bulk_schedule_migration(start: start, finish: finish) + + logger.info( + migrator: 'MigrateToHashedStorage', + message: "Enqueuing migration of projects in batches of #{batch_size} from ID=#{start} to ID=#{finish}", + batch_from: start, + batch_to: finish + ) + end + end + + private + + def helper + Gitlab::HashedStorage::RakeHelper + end + + def storage_migrator + @storage_migrator ||= Gitlab::HashedStorage::Migrator.new + end + + def logger + @logger ||= ::Gitlab::BackgroundMigration::Logger.build + end + end + end +end diff --git a/lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb b/lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb new file mode 100644 index 00000000000..eb72ef1de33 --- /dev/null +++ b/lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # rubocop:disable Style/Documentation + class PopulateResolvedOnDefaultBranchColumn + def perform(*); end + end + end +end + +Gitlab::BackgroundMigration::PopulateResolvedOnDefaultBranchColumn.prepend_if_ee('EE::Gitlab::BackgroundMigration::PopulateResolvedOnDefaultBranchColumn') diff --git a/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb b/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb new file mode 100644 index 00000000000..a0c89cc4664 --- /dev/null +++ b/lib/gitlab/background_migration/populate_vulnerability_historical_statistics.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # 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) + end + end + end +end + +Gitlab::BackgroundMigration::PopulateVulnerabilityHistoricalStatistics.prepend_if_ee('EE::Gitlab::BackgroundMigration::PopulateVulnerabilityHistoricalStatistics') diff --git a/lib/gitlab/background_migration/remove_duplicate_cs_findings.rb b/lib/gitlab/background_migration/remove_duplicate_cs_findings.rb new file mode 100644 index 00000000000..cc9b0329556 --- /dev/null +++ b/lib/gitlab/background_migration/remove_duplicate_cs_findings.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class RemoveDuplicateCsFindings + def perform(start_id, stop_id) + end + end + end +end + +Gitlab::BackgroundMigration::RemoveDuplicateCsFindings.prepend_if_ee('EE::Gitlab::BackgroundMigration::RemoveDuplicateCsFindings') diff --git a/lib/gitlab/background_migration/set_merge_request_diff_files_count.rb b/lib/gitlab/background_migration/set_merge_request_diff_files_count.rb index 9f765d03d62..527dd2a0a83 100644 --- a/lib/gitlab/background_migration/set_merge_request_diff_files_count.rb +++ b/lib/gitlab/background_migration/set_merge_request_diff_files_count.rb @@ -4,13 +4,18 @@ module Gitlab module BackgroundMigration # Sets the MergeRequestDiff#files_count value for old rows class SetMergeRequestDiffFilesCount - COUNT_SUBQUERY = <<~SQL - files_count = ( - SELECT count(*) - FROM merge_request_diff_files - WHERE merge_request_diff_files.merge_request_diff_id = merge_request_diffs.id - ) - SQL + # Some historic data has a *lot* of files. Apply a sentinel to these cases + FILES_COUNT_SENTINEL = 2**15 - 1 + + def self.count_subquery + <<~SQL + files_count = ( + SELECT LEAST(#{FILES_COUNT_SENTINEL}, count(*)) + FROM merge_request_diff_files + WHERE merge_request_diff_files.merge_request_diff_id = merge_request_diffs.id + ) + SQL + end class MergeRequestDiff < ActiveRecord::Base # rubocop:disable Style/Documentation include EachBatch @@ -20,7 +25,7 @@ module Gitlab def perform(start_id, end_id) MergeRequestDiff.where(id: start_id..end_id).each_batch do |relation| - relation.update_all(COUNT_SUBQUERY) + relation.update_all(self.class.count_subquery) end end end diff --git a/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value.rb b/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value.rb index 9ac92aab637..c485c23f3be 100644 --- a/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value.rb +++ b/lib/gitlab/background_migration/set_null_package_files_file_store_to_local_value.rb @@ -11,9 +11,11 @@ module Gitlab class SetNullPackageFilesFileStoreToLocalValue LOCAL_STORE = 1 # equal to ObjectStorage::Store::LOCAL - # Temporary AR class for package files - class PackageFile < ActiveRecord::Base - self.table_name = 'packages_package_files' + module Packages + # Temporary AR class for package files + class PackageFile < ActiveRecord::Base + self.table_name = 'packages_package_files' + end end def perform(start_id, stop_id) diff --git a/lib/gitlab/background_migration/update_location_fingerprint_for_container_scanning_findings.rb b/lib/gitlab/background_migration/update_location_fingerprint_for_container_scanning_findings.rb new file mode 100644 index 00000000000..651df36fcfd --- /dev/null +++ b/lib/gitlab/background_migration/update_location_fingerprint_for_container_scanning_findings.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class UpdateLocationFingerprintForContainerScanningFindings + def perform(start_id, stop_id) + end + end + end +end + +Gitlab::BackgroundMigration::UpdateLocationFingerprintForContainerScanningFindings.prepend_if_ee('EE::Gitlab::BackgroundMigration::UpdateLocationFingerprintForContainerScanningFindings') diff --git a/lib/gitlab/backtrace_cleaner.rb b/lib/gitlab/backtrace_cleaner.rb index d04f0983d12..caea05c720d 100644 --- a/lib/gitlab/backtrace_cleaner.rb +++ b/lib/gitlab/backtrace_cleaner.rb @@ -31,7 +31,7 @@ module Gitlab return unless backtrace Array(Rails.backtrace_cleaner.clean(backtrace)).reject do |line| - line.match(IGNORED_BACKTRACES_REGEXP) + IGNORED_BACKTRACES_REGEXP.match?(line) end end end diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb index 6b78825aefd..1b985f83b22 100644 --- a/lib/gitlab/badge/coverage/template.rb +++ b/lib/gitlab/badge/coverage/template.rb @@ -25,7 +25,7 @@ module Gitlab end def key_text - if @key_text && @key_text.size <= MAX_KEY_SIZE + if @key_text && @key_text.size <= MAX_KEY_TEXT_SIZE @key_text else @entity.to_s @@ -37,7 +37,7 @@ module Gitlab end def key_width - if @key_width && @key_width.between?(1, MAX_KEY_SIZE) + if @key_width && @key_width.between?(1, MAX_KEY_WIDTH) @key_width else 62 diff --git a/lib/gitlab/badge/pipeline/status.rb b/lib/gitlab/badge/pipeline/status.rb index 17f179f027d..f061ba22688 100644 --- a/lib/gitlab/badge/pipeline/status.rb +++ b/lib/gitlab/badge/pipeline/status.rb @@ -12,6 +12,7 @@ module Gitlab def initialize(project, ref, opts: {}) @project = project @ref = ref + @ignore_skipped = Gitlab::Utils.to_boolean(opts[:ignore_skipped], default: false) @customization = { key_width: opts[:key_width].to_i, key_text: opts[:key_text] @@ -26,9 +27,11 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def status - @project.ci_pipelines + pipelines = @project.ci_pipelines .where(sha: @sha) - .latest_status(@ref) || 'unknown' + + relation = @ignore_skipped ? pipelines.without_statuses([:skipped]) : pipelines + relation.latest_status(@ref) || 'unknown' end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/gitlab/badge/pipeline/template.rb b/lib/gitlab/badge/pipeline/template.rb index 781897fab4b..af8e318395b 100644 --- a/lib/gitlab/badge/pipeline/template.rb +++ b/lib/gitlab/badge/pipeline/template.rb @@ -29,7 +29,7 @@ module Gitlab end def key_text - if @key_text && @key_text.size <= MAX_KEY_SIZE + if @key_text && @key_text.size <= MAX_KEY_TEXT_SIZE @key_text else @entity.to_s @@ -41,7 +41,7 @@ module Gitlab end def key_width - if @key_width && @key_width.between?(1, MAX_KEY_SIZE) + if @key_width && @key_width.between?(1, MAX_KEY_WIDTH) @key_width else 62 diff --git a/lib/gitlab/badge/template.rb b/lib/gitlab/badge/template.rb index 97103e3f42c..9ac8f1c17f2 100644 --- a/lib/gitlab/badge/template.rb +++ b/lib/gitlab/badge/template.rb @@ -6,7 +6,8 @@ module Gitlab # Abstract template class for badges # class Template - MAX_KEY_SIZE = 128 + MAX_KEY_TEXT_SIZE = 64 + MAX_KEY_WIDTH = 512 def initialize(badge) @entity = badge.entity diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb index 18a1b64729e..aca5a63a424 100644 --- a/lib/gitlab/bitbucket_server_import/importer.rb +++ b/lib/gitlab/bitbucket_server_import/importer.rb @@ -61,17 +61,18 @@ module Gitlab }.to_json) end - def gitlab_user_id(email) - find_user_id(email) || project.creator_id - end + def find_user_id(by:, value:) + return unless value - def find_user_id(email) - return unless email + return users[value] if users.key?(value) - return users[email] if users.key?(email) + user = if by == :email + User.find_by_any_email(value, confirmed: true) + else + User.find_by_username(value) + end - user = User.find_by_any_email(email, confirmed: true) - users[email] = user&.id + users[value] = user&.id user&.id end @@ -197,9 +198,8 @@ module Gitlab log_info(stage: 'import_bitbucket_pull_requests', message: 'starting', iid: pull_request.iid) description = '' - description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author_email) + description += author_line(pull_request) description += pull_request.description if pull_request.description - author_id = gitlab_user_id(pull_request.author_email) attributes = { iid: pull_request.iid, @@ -212,7 +212,7 @@ module Gitlab target_branch: Gitlab::Git.ref_name(pull_request.target_branch_name), target_branch_sha: pull_request.target_branch_sha, state_id: MergeRequest.available_states[pull_request.state], - author_id: author_id, + author_id: author_id(pull_request), created_at: pull_request.created_at, updated_at: pull_request.updated_at } @@ -254,7 +254,7 @@ module Gitlab committer = merge_event.committer_email - user_id = gitlab_user_id(committer) + user_id = find_user_id(by: :email, value: committer) || project.creator_id timestamp = merge_event.merge_timestamp merge_request.update({ merge_commit_sha: merge_event.merge_commit }) metric = MergeRequest::Metrics.find_or_initialize_by(merge_request: merge_request) @@ -353,7 +353,7 @@ module Gitlab end def pull_request_comment_attributes(comment) - author = find_user_id(comment.author_email) + author = uid(comment) note = '' unless author @@ -397,6 +397,23 @@ module Gitlab def metrics @metrics ||= Gitlab::Import::Metrics.new(:bitbucket_server_importer, @project) end + + def author_line(rep_object) + return '' if uid(rep_object) + + @formatter.author_line(rep_object.author) + end + + def author_id(rep_object) + uid(rep_object) || project.creator_id + end + + def uid(rep_object) + find_user_id(by: :email, value: rep_object.author_email) unless Feature.enabled?(:bitbucket_server_user_mapping_by_username) + + find_user_id(by: :username, value: rep_object.author_username) || + find_user_id(by: :email, value: rep_object.author_email) + end end end end diff --git a/lib/gitlab/cache/request_cache.rb b/lib/gitlab/cache/request_cache.rb index 6e48ca90054..3ad919fbba8 100644 --- a/lib/gitlab/cache/request_cache.rb +++ b/lib/gitlab/cache/request_cache.rb @@ -55,7 +55,7 @@ module Gitlab .join(':') end - private cache_key_method_name + private cache_key_method_name # rubocop: disable Style/AccessModifierDeclarations end end end diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb index e18cf6ff8f2..78952db7a3e 100644 --- a/lib/gitlab/checks/lfs_integrity.rb +++ b/lib/gitlab/checks/lfs_integrity.rb @@ -17,7 +17,7 @@ module Gitlab return false unless new_lfs_pointers.present? - existing_count = @project.all_lfs_objects + existing_count = @project.lfs_objects .for_oids(new_lfs_pointers.map(&:lfs_oid)) .count diff --git a/lib/gitlab/checks/snippet_check.rb b/lib/gitlab/checks/snippet_check.rb index bcecd0fc251..8c61b782baa 100644 --- a/lib/gitlab/checks/snippet_check.rb +++ b/lib/gitlab/checks/snippet_check.rb @@ -3,7 +3,6 @@ module Gitlab module Checks class SnippetCheck < BaseChecker - DEFAULT_BRANCH = 'master'.freeze ERROR_MESSAGES = { create_delete_branch: 'You can not create or delete branches.' }.freeze @@ -11,17 +10,18 @@ module Gitlab ATTRIBUTES = %i[oldrev newrev ref branch_name tag_name logger].freeze attr_reader(*ATTRIBUTES) - def initialize(change, logger:) + def initialize(change, default_branch:, 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 @logger = logger @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}") end def validate! - if creation? || deletion? + if !@default_branch || creation? || deletion? raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_delete_branch] end @@ -31,7 +31,7 @@ module Gitlab private def creation? - @branch_name != DEFAULT_BRANCH && super + @branch_name != @default_branch && super end end end diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb index e145bd2e9df..1fac00337a3 100644 --- a/lib/gitlab/ci/ansi2html.rb +++ b/lib/gitlab/ci/ansi2html.rb @@ -31,105 +31,205 @@ module Gitlab end class Converter - def on_0(_) reset end + def on_0(_) + reset + end - def on_1(_) enable(STYLE_SWITCHES[:bold]) end + def on_1(_) + enable(STYLE_SWITCHES[:bold]) + end - def on_3(_) enable(STYLE_SWITCHES[:italic]) end + def on_3(_) + enable(STYLE_SWITCHES[:italic]) + end - def on_4(_) enable(STYLE_SWITCHES[:underline]) end + def on_4(_) + enable(STYLE_SWITCHES[:underline]) + end - def on_8(_) enable(STYLE_SWITCHES[:conceal]) end + def on_8(_) + enable(STYLE_SWITCHES[:conceal]) + end - def on_9(_) enable(STYLE_SWITCHES[:cross]) end + def on_9(_) + enable(STYLE_SWITCHES[:cross]) + end - def on_21(_) disable(STYLE_SWITCHES[:bold]) end + def on_21(_) + disable(STYLE_SWITCHES[:bold]) + end - def on_22(_) disable(STYLE_SWITCHES[:bold]) end + def on_22(_) + disable(STYLE_SWITCHES[:bold]) + end - def on_23(_) disable(STYLE_SWITCHES[:italic]) end + def on_23(_) + disable(STYLE_SWITCHES[:italic]) + end - def on_24(_) disable(STYLE_SWITCHES[:underline]) end + def on_24(_) + disable(STYLE_SWITCHES[:underline]) + end - def on_28(_) disable(STYLE_SWITCHES[:conceal]) end + def on_28(_) + disable(STYLE_SWITCHES[:conceal]) + end - def on_29(_) disable(STYLE_SWITCHES[:cross]) end + def on_29(_) + disable(STYLE_SWITCHES[:cross]) + end - def on_30(_) set_fg_color(0) end + def on_30(_) + set_fg_color(0) + end - def on_31(_) set_fg_color(1) end + def on_31(_) + set_fg_color(1) + end - def on_32(_) set_fg_color(2) end + def on_32(_) + set_fg_color(2) + end - def on_33(_) set_fg_color(3) end + def on_33(_) + set_fg_color(3) + end - def on_34(_) set_fg_color(4) end + def on_34(_) + set_fg_color(4) + end - def on_35(_) set_fg_color(5) end + def on_35(_) + set_fg_color(5) + end - def on_36(_) set_fg_color(6) end + def on_36(_) + set_fg_color(6) + end - def on_37(_) set_fg_color(7) end + def on_37(_) + set_fg_color(7) + end - def on_38(stack) set_fg_color_256(stack) end + def on_38(stack) + set_fg_color_256(stack) + end - def on_39(_) set_fg_color(9) end + def on_39(_) + set_fg_color(9) + end - def on_40(_) set_bg_color(0) end + def on_40(_) + set_bg_color(0) + end - def on_41(_) set_bg_color(1) end + def on_41(_) + set_bg_color(1) + end - def on_42(_) set_bg_color(2) end + def on_42(_) + set_bg_color(2) + end - def on_43(_) set_bg_color(3) end + def on_43(_) + set_bg_color(3) + end - def on_44(_) set_bg_color(4) end + def on_44(_) + set_bg_color(4) + end - def on_45(_) set_bg_color(5) end + def on_45(_) + set_bg_color(5) + end - def on_46(_) set_bg_color(6) end + def on_46(_) + set_bg_color(6) + end - def on_47(_) set_bg_color(7) end + def on_47(_) + set_bg_color(7) + end - def on_48(stack) set_bg_color_256(stack) end + def on_48(stack) + set_bg_color_256(stack) + end - def on_49(_) set_bg_color(9) end + def on_49(_) + set_bg_color(9) + end - def on_90(_) set_fg_color(0, 'l') end + def on_90(_) + set_fg_color(0, 'l') + end - def on_91(_) set_fg_color(1, 'l') end + def on_91(_) + set_fg_color(1, 'l') + end - def on_92(_) set_fg_color(2, 'l') end + def on_92(_) + set_fg_color(2, 'l') + end - def on_93(_) set_fg_color(3, 'l') end + def on_93(_) + set_fg_color(3, 'l') + end - def on_94(_) set_fg_color(4, 'l') end + def on_94(_) + set_fg_color(4, 'l') + end - def on_95(_) set_fg_color(5, 'l') end + def on_95(_) + set_fg_color(5, 'l') + end - def on_96(_) set_fg_color(6, 'l') end + def on_96(_) + set_fg_color(6, 'l') + end - def on_97(_) set_fg_color(7, 'l') end + def on_97(_) + set_fg_color(7, 'l') + end - def on_99(_) set_fg_color(9, 'l') end + def on_99(_) + set_fg_color(9, 'l') + end - def on_100(_) set_bg_color(0, 'l') end + def on_100(_) + set_bg_color(0, 'l') + end - def on_101(_) set_bg_color(1, 'l') end + def on_101(_) + set_bg_color(1, 'l') + end - def on_102(_) set_bg_color(2, 'l') end + def on_102(_) + set_bg_color(2, 'l') + end - def on_103(_) set_bg_color(3, 'l') end + def on_103(_) + set_bg_color(3, 'l') + end - def on_104(_) set_bg_color(4, 'l') end + def on_104(_) + set_bg_color(4, 'l') + end - def on_105(_) set_bg_color(5, 'l') end + def on_105(_) + set_bg_color(5, 'l') + end - def on_106(_) set_bg_color(6, 'l') end + def on_106(_) + set_bg_color(6, 'l') + end - def on_107(_) set_bg_color(7, 'l') end + def on_107(_) + set_bg_color(7, 'l') + end - def on_109(_) set_bg_color(9, 'l') end + def on_109(_) + set_bg_color(9, 'l') + end attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask, :sections, :lineno_in_section diff --git a/lib/gitlab/ci/artifact_file_reader.rb b/lib/gitlab/ci/artifact_file_reader.rb index c2d17cc176e..6395a20ca99 100644 --- a/lib/gitlab/ci/artifact_file_reader.rb +++ b/lib/gitlab/ci/artifact_file_reader.rb @@ -45,6 +45,31 @@ module Gitlab end def read_zip_file!(file_path) + if ::Gitlab::Ci::Features.new_artifact_file_reader_enabled?(job.project) + read_with_new_artifact_file_reader(file_path) + else + read_with_legacy_artifact_file_reader(file_path) + end + end + + def read_with_new_artifact_file_reader(file_path) + job.artifacts_file.use_open_file do |file| + zip_file = Zip::File.new(file, false, true) + entry = zip_file.find_entry(file_path) + + unless entry + raise Error, "Path `#{file_path}` does not exist inside the `#{job.name}` artifacts archive!" + end + + if entry.name_is_directory? + raise Error, "Path `#{file_path}` was expected to be a file but it was a directory!" + end + + zip_file.read(entry) + end + end + + def read_with_legacy_artifact_file_reader(file_path) job.artifacts_file.use_file do |archive_path| Zip::File.open(archive_path) do |zip_file| entry = zip_file.find_entry(file_path) diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb index b64990d6a7a..72ef0a8d067 100644 --- a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb +++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb @@ -33,7 +33,7 @@ module Gitlab def kubernetes_namespace strong_memoize(:kubernetes_namespace) do - Clusters::KubernetesNamespaceFinder.new( + ::Clusters::KubernetesNamespaceFinder.new( deployment_cluster, project: environment.project, environment_name: environment.name, @@ -47,7 +47,7 @@ module Gitlab return if conflicting_ci_namespace_requested?(namespace) - Clusters::Kubernetes::CreateOrUpdateNamespaceService.new( + ::Clusters::Kubernetes::CreateOrUpdateNamespaceService.new( cluster: deployment_cluster, kubernetes_namespace: namespace ).execute @@ -71,7 +71,7 @@ module Gitlab end def build_namespace_record - Clusters::BuildKubernetesNamespaceService.new( + ::Clusters::BuildKubernetesNamespaceService.new( deployment_cluster, environment: environment ).execute diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index d81a3fef1f5..9d269831679 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -62,6 +62,10 @@ module Gitlab root.jobs_value end + def normalized_jobs + @normalized_jobs ||= Ci::Config::Normalizer.new(jobs).normalize_jobs + end + private def expand_config(config) diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index f960cec1f26..ecc2c5cb729 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -122,39 +122,9 @@ module Gitlab :needs, :retry, :parallel, :start_in, :interruptible, :timeout, :resource_group, :release - Matcher = Struct.new(:name, :config) do - def applies? - job_is_not_hidden? && - config_is_a_hash? && - has_job_keys? - end - - private - - def job_is_not_hidden? - !name.to_s.start_with?('.') - end - - def config_is_a_hash? - config.is_a?(Hash) - end - - def has_job_keys? - if name == :default - config.key?(:script) - else - (ALLOWED_KEYS & config.keys).any? - end - end - end - def self.matching?(name, config) - if Gitlab::Ci::Features.job_entry_matches_all_keys? - Matcher.new(name, config).applies? - else - !name.to_s.start_with?('.') && - config.is_a?(Hash) && config.key?(:script) - end + !name.to_s.start_with?('.') && + config.is_a?(Hash) && config.key?(:script) end def self.visible? diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb index 1d3036189b0..b5ce42969a5 100644 --- a/lib/gitlab/ci/config/entry/jobs.rb +++ b/lib/gitlab/ci/config/entry/jobs.rb @@ -14,8 +14,8 @@ module Gitlab validates :config, type: Hash validate do - unless has_valid_jobs? - errors.add(:config, 'should contain valid jobs') + each_unmatched_job do |name| + errors.add(name, 'config should implement a script: or a trigger: keyword') end unless has_visible_job? @@ -23,9 +23,9 @@ module Gitlab end end - def has_valid_jobs? - config.all? do |name, value| - Jobs.find_type(name, value) + def each_unmatched_job + config.each do |name, value| + yield(name) unless Jobs.find_type(name, value) end end diff --git a/lib/gitlab/ci/config/entry/root.rb b/lib/gitlab/ci/config/entry/root.rb index 19d6a470941..2d93f1ab06e 100644 --- a/lib/gitlab/ci/config/entry/root.rb +++ b/lib/gitlab/ci/config/entry/root.rb @@ -134,7 +134,7 @@ module Gitlab @jobs_config = @config .except(*self.class.reserved_nodes_names) .select do |name, config| - Entry::Jobs.find_type(name, config).present? + Entry::Jobs.find_type(name, config).present? || ALLOWED_KEYS.exclude?(name) end @config = @config.except(*@jobs_config.keys) diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb index 451ba14bb89..22fcd84c968 100644 --- a/lib/gitlab/ci/config/normalizer.rb +++ b/lib/gitlab/ci/config/normalizer.rb @@ -11,6 +11,7 @@ module Gitlab end def normalize_jobs + return {} unless @jobs_config return @jobs_config if parallelized_jobs.empty? expand_parallelize_jobs do |job_name, config| diff --git a/lib/gitlab/ci/config/normalizer/matrix_strategy.rb b/lib/gitlab/ci/config/normalizer/matrix_strategy.rb index db21274a9ed..5a23836d8a0 100644 --- a/lib/gitlab/ci/config/normalizer/matrix_strategy.rb +++ b/lib/gitlab/ci/config/normalizer/matrix_strategy.rb @@ -48,14 +48,13 @@ module Gitlab } end - def name_with_details - vars = variables.map { |key, value| "#{key}=#{value}"}.join('; ') - - "#{job_name} (#{vars})" - end - def name - "#{job_name} #{instance}/#{total}" + vars = variables + .values + .compact + .join(', ') + + "#{job_name}: [#{vars}]" end private diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index 2f6667d3600..e770187b124 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -31,56 +31,49 @@ module Gitlab ::Feature.enabled?(:ci_store_pipeline_messages, project, default_enabled: true) end - # Remove in https://gitlab.com/gitlab-org/gitlab/-/issues/227052 - def self.variables_api_filter_environment_scope? - ::Feature.enabled?(:ci_variables_api_filter_environment_scope, default_enabled: true) - end - def self.raise_job_rules_without_workflow_rules_warning? ::Feature.enabled?(:ci_raise_job_rules_without_workflow_rules_warning, default_enabled: true) end - def self.keep_latest_artifacts_for_ref_enabled?(project) - ::Feature.enabled?(:keep_latest_artifacts_for_ref, project, default_enabled: false) - end - - def self.destroy_only_unlocked_expired_artifacts_enabled? - ::Feature.enabled?(:destroy_only_unlocked_expired_artifacts, default_enabled: false) - end - def self.bulk_insert_on_create?(project) ::Feature.enabled?(:ci_bulk_insert_on_create, project, default_enabled: true) end - def self.ci_if_parenthesis_enabled? - ::Feature.enabled?(:ci_if_parenthesis_enabled, default_enabled: true) + # NOTE: The feature flag `disallow_to_create_merge_request_pipelines_in_target_project` + # is a safe switch to disable the feature for a parituclar project when something went wrong, + # therefore it's not supposed to be enabled by default. + def self.disallow_to_create_merge_request_pipelines_in_target_project?(target_project) + ::Feature.enabled?(:ci_disallow_to_create_merge_request_pipelines_in_target_project, target_project) end - def self.allow_to_create_merge_request_pipelines_in_target_project?(target_project) - ::Feature.enabled?(:ci_allow_to_create_merge_request_pipelines_in_target_project, target_project, default_enabled: true) + def self.lint_creates_pipeline_with_dry_run?(project) + ::Feature.enabled?(:ci_lint_creates_pipeline_with_dry_run, project, default_enabled: true) end - def self.ci_plan_needs_size_limit?(project) - ::Feature.enabled?(:ci_plan_needs_size_limit, project, default_enabled: true) + def self.project_transactionless_destroy?(project) + Feature.enabled?(:project_transactionless_destroy, project, default_enabled: false) end - def self.job_entry_matches_all_keys? - ::Feature.enabled?(:ci_job_entry_matches_all_keys) + def self.coverage_report_view?(project) + ::Feature.enabled?(:coverage_report_view, project, default_enabled: true) end - def self.lint_creates_pipeline_with_dry_run?(project) - ::Feature.enabled?(:ci_lint_creates_pipeline_with_dry_run, project, default_enabled: true) + def self.child_of_child_pipeline_enabled?(project) + ::Feature.enabled?(:ci_child_of_child_pipeline, project, default_enabled: true) end - def self.reset_ci_minutes_for_all_namespaces? - ::Feature.enabled?(:reset_ci_minutes_for_all_namespaces, default_enabled: false) + def self.trace_overwrite? + ::Feature.enabled?(:ci_trace_overwrite, type: :ops, default_enabled: false) end - def self.expand_names_for_cross_pipeline_artifacts?(project) - ::Feature.enabled?(:ci_expand_names_for_cross_pipeline_artifacts, project) + def self.accept_trace?(project) + ::Feature.enabled?(:ci_enable_live_trace, project) && + ::Feature.enabled?(:ci_accept_trace, project, type: :ops, default_enabled: false) + end + + def self.new_artifact_file_reader_enabled?(project) + ::Feature.enabled?(:ci_new_artifact_file_reader, project, default_enabled: false) end end end end - -::Gitlab::Ci::Features.prepend_if_ee('::EE::Gitlab::Ci::Features') diff --git a/lib/gitlab/ci/lint.rb b/lib/gitlab/ci/lint.rb new file mode 100644 index 00000000000..86a9ebfa451 --- /dev/null +++ b/lib/gitlab/ci/lint.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Lint + class Result + attr_reader :jobs, :errors, :warnings + + def initialize(jobs:, errors:, warnings:) + @jobs = jobs + @errors = errors + @warnings = warnings + end + + def valid? + @errors.empty? + end + end + + def initialize(project:, current_user:) + @project = project + @current_user = current_user + end + + def validate(content, dry_run: false) + if dry_run && Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project) + simulate_pipeline_creation(content) + else + static_validation(content) + end + end + + private + + def simulate_pipeline_creation(content) + pipeline = ::Ci::CreatePipelineService + .new(@project, @current_user, ref: @project.default_branch) + .execute(:push, dry_run: true, content: content) + + Result.new( + jobs: dry_run_convert_to_jobs(pipeline.stages), + errors: pipeline.error_messages.map(&:content), + warnings: pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content) + ) + end + + def static_validation(content) + result = Gitlab::Ci::YamlProcessor.new( + content, + project: @project, + user: @current_user, + sha: @project.repository.commit.sha + ).execute + + Result.new( + jobs: static_validation_convert_to_jobs(result), + errors: result.errors, + warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord + ) + end + + def dry_run_convert_to_jobs(stages) + stages.reduce([]) do |jobs, stage| + jobs + stage.statuses.map do |job| + { + name: job.name, + stage: stage.name, + before_script: job.options[:before_script].to_a, + script: job.options[:script].to_a, + after_script: job.options[:after_script].to_a, + tag_list: (job.tag_list if job.is_a?(::Ci::Build)).to_a, + environment: job.options.dig(:environment, :name), + when: job.when, + allow_failure: job.allow_failure + } + end + end + end + + def static_validation_convert_to_jobs(result) + jobs = [] + return jobs unless result.valid? + + result.stages.each do |stage_name| + result.builds.each do |job| + next unless job[:stage] == stage_name + + jobs << { + name: job[:name], + stage: stage_name, + before_script: job.dig(:options, :before_script).to_a, + script: job.dig(:options, :script).to_a, + after_script: job.dig(:options, :after_script).to_a, + tag_list: job[:tag_list].to_a, + only: job[:only], + except: job[:except], + environment: job[:environment], + when: job[:when], + allow_failure: job[:allow_failure] + } + end + end + + jobs + end + end + end +end diff --git a/lib/gitlab/ci/mask_secret.rb b/lib/gitlab/ci/mask_secret.rb index 58d55b1bd6f..e5a7151b823 100644 --- a/lib/gitlab/ci/mask_secret.rb +++ b/lib/gitlab/ci/mask_secret.rb @@ -8,6 +8,11 @@ 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 end diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb index 4190c40eb66..9662209f88e 100644 --- a/lib/gitlab/ci/pipeline/chain/build.rb +++ b/lib/gitlab/ci/pipeline/chain/build.rb @@ -20,11 +20,7 @@ module Gitlab pipeline_schedule: @command.schedule, merge_request: @command.merge_request, external_pull_request: @command.external_pull_request, - variables_attributes: Array(@command.variables_attributes), - # This should be removed and set on the database column default - # level when the keep_latest_artifacts_for_ref feature flag is - # removed. - locked: ::Gitlab::Ci::Features.keep_latest_artifacts_for_ref_enabled?(@command.project) ? :artifacts_locked : :unlocked + variables_attributes: Array(@command.variables_attributes) ) end diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index dbaa6951e64..d1882059dd8 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, :config_processor, :stage_seeds + :config_content, :yaml_processor_result, :stage_seeds ) do include Gitlab::Utils::StrongMemoize diff --git a/lib/gitlab/ci/pipeline/chain/config/content/remote.rb b/lib/gitlab/ci/pipeline/chain/config/content/remote.rb index dcc336b8929..4990a5a6eb5 100644 --- a/lib/gitlab/ci/pipeline/chain/config/content/remote.rb +++ b/lib/gitlab/ci/pipeline/chain/config/content/remote.rb @@ -9,7 +9,7 @@ module Gitlab class Remote < Source def content strong_memoize(:content) do - next unless ci_config_path =~ URI.regexp(%w[http https]) + next unless ci_config_path =~ URI::DEFAULT_PARSER.make_regexp(%w[http https]) YAML.dump('include' => [{ 'remote' => ci_config_path }]) end diff --git a/lib/gitlab/ci/pipeline/chain/config/process.rb b/lib/gitlab/ci/pipeline/chain/config/process.rb index 2cfcb295407..8ccb33ffd34 100644 --- a/lib/gitlab/ci/pipeline/chain/config/process.rb +++ b/lib/gitlab/ci/pipeline/chain/config/process.rb @@ -11,20 +11,23 @@ module Gitlab def perform! raise ArgumentError, 'missing config content' unless @command.config_content - @command.config_processor = ::Gitlab::Ci::YamlProcessor.new( + result = ::Gitlab::Ci::YamlProcessor.new( @command.config_content, { project: project, sha: @pipeline.sha, user: current_user, parent_pipeline: parent_pipeline } - ) + ).execute + + add_warnings_to_pipeline(result.warnings) - add_warnings_to_pipeline(@command.config_processor.warnings) - rescue Gitlab::Ci::YamlProcessor::ValidationError => ex - add_warnings_to_pipeline(ex.warnings) + if result.valid? + @command.yaml_processor_result = result + else + error(result.errors.first, config_error: true) + end - error(ex.message, config_error: true) rescue => ex Gitlab::ErrorTracking.track_exception(ex, project_id: project.id, diff --git a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb index a793ae9cc24..3c910963a2a 100644 --- a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb +++ b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb @@ -39,7 +39,7 @@ module Gitlab end def workflow_config - @command.config_processor.workflow_attributes || {} + @command.yaml_processor_result.workflow_attributes || {} end end end diff --git a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb index 9267c72efa4..71f22c52869 100644 --- a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb +++ b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb @@ -6,13 +6,13 @@ module Gitlab module Chain class RemoveUnwantedChatJobs < Chain::Base def perform! - raise ArgumentError, 'missing config processor' unless @command.config_processor + raise ArgumentError, 'missing YAML processor result' unless @command.yaml_processor_result return unless pipeline.chat? # When scheduling a chat pipeline we only want to run the build # that matches the chat command. - @command.config_processor.jobs.select! do |name, _| + @command.yaml_processor_result.jobs.select! do |name, _| name.to_s == command.chat_data[:command].to_s end end diff --git a/lib/gitlab/ci/pipeline/chain/seed.rb b/lib/gitlab/ci/pipeline/chain/seed.rb index e48e79d561b..e10a0bc3718 100644 --- a/lib/gitlab/ci/pipeline/chain/seed.rb +++ b/lib/gitlab/ci/pipeline/chain/seed.rb @@ -9,7 +9,7 @@ module Gitlab include Gitlab::Utils::StrongMemoize def perform! - raise ArgumentError, 'missing config processor' unless @command.config_processor + raise ArgumentError, 'missing YAML processor result' unless @command.yaml_processor_result # Allocate next IID. This operation must be outside of transactions of pipeline creations. pipeline.ensure_project_iid! @@ -56,7 +56,7 @@ module Gitlab end def stages_attributes - @command.config_processor.stages_attributes + @command.yaml_processor_result.stages_attributes end end end diff --git a/lib/gitlab/ci/pipeline/chain/validate/external.rb b/lib/gitlab/ci/pipeline/chain/validate/external.rb index 24628338dd2..d056501a6d3 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/external.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/external.rb @@ -51,7 +51,7 @@ module Gitlab def validate_service_request Gitlab::HTTP.post( validation_service_url, timeout: VALIDATION_REQUEST_TIMEOUT, - body: validation_service_payload(@pipeline, @command.config_processor.stages_attributes) + body: validation_service_payload(@pipeline, @command.yaml_processor_result.stages_attributes) ) end diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb index 5b7365cb33b..ac03ef79ccb 100644 --- a/lib/gitlab/ci/pipeline/expression/lexer.rb +++ b/lib/gitlab/ci/pipeline/expression/lexer.rb @@ -24,26 +24,8 @@ module Gitlab Expression::Lexeme::Or ].freeze - # To be removed with `ci_if_parenthesis_enabled` - LEGACY_LEXEMES = [ - Expression::Lexeme::Variable, - Expression::Lexeme::String, - Expression::Lexeme::Pattern, - Expression::Lexeme::Null, - Expression::Lexeme::Equals, - Expression::Lexeme::Matches, - Expression::Lexeme::NotEquals, - Expression::Lexeme::NotMatches, - Expression::Lexeme::And, - Expression::Lexeme::Or - ].freeze - def self.lexemes - if ::Gitlab::Ci::Features.ci_if_parenthesis_enabled? - LEXEMES - else - LEGACY_LEXEMES - end + LEXEMES end MAX_TOKENS = 100 diff --git a/lib/gitlab/ci/pipeline/expression/parser.rb b/lib/gitlab/ci/pipeline/expression/parser.rb index 27d7aa2f37e..a20b0015e05 100644 --- a/lib/gitlab/ci/pipeline/expression/parser.rb +++ b/lib/gitlab/ci/pipeline/expression/parser.rb @@ -15,12 +15,7 @@ module Gitlab def tree results = [] - tokens = - if ::Gitlab::Ci::Features.ci_if_parenthesis_enabled? - tokens_rpn - else - legacy_tokens_rpn - end + tokens = tokens_rpn tokens.each do |token| case token.type @@ -78,27 +73,6 @@ module Gitlab output.concat(operators.reverse) end - - # To be removed with `ci_if_parenthesis_enabled` - def legacy_tokens_rpn - output = [] - operators = [] - - @tokens.each do |token| - case token.type - when :value - output.push(token) - when :logical_operator - if operators.any? && token.lexeme.precedence >= operators.last.lexeme.precedence - output.push(operators.pop) - end - - operators.push(token) - end - end - - output.concat(operators.reverse) - end end end end diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index 3be3fa63b92..91dbcc616ea 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -11,8 +11,6 @@ module Gitlab delegate :dig, to: :@seed_attributes - DEFAULT_NEEDS_LIMIT = 10 - def initialize(pipeline, attributes, previous_stages) @pipeline = pipeline @seed_attributes = attributes @@ -140,11 +138,7 @@ module Gitlab end def max_needs_allowed - if ::Gitlab::Ci::Features.ci_plan_needs_size_limit?(@pipeline.project) - @pipeline.project.actual_limits.ci_needs_size_limit - else - DEFAULT_NEEDS_LIMIT - end + @pipeline.project.actual_limits.ci_needs_size_limit end def pipeline_attributes diff --git a/lib/gitlab/ci/pipeline_object_hierarchy.rb b/lib/gitlab/ci/pipeline_object_hierarchy.rb new file mode 100644 index 00000000000..de3262b10e0 --- /dev/null +++ b/lib/gitlab/ci/pipeline_object_hierarchy.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class PipelineObjectHierarchy < ::Gitlab::ObjectHierarchy + private + + def middle_table + ::Ci::Sources::Pipeline.arel_table + end + + def from_tables(cte) + [objects_table, cte.table, middle_table] + end + + def parent_id_column(_cte) + middle_table[:source_pipeline_id] + end + + def ancestor_conditions(cte) + middle_table[:source_pipeline_id].eq(objects_table[:id]).and( + middle_table[:pipeline_id].eq(cte.table[:id]) + ).and( + same_project_condition + ) + end + + def descendant_conditions(cte) + middle_table[:pipeline_id].eq(objects_table[:id]).and( + middle_table[:source_pipeline_id].eq(cte.table[:id]) + ).and( + same_project_condition + ) + end + + def same_project_condition + if options[:same_project] + middle_table[:source_project_id].eq(middle_table[:project_id]) + else + Arel.sql('TRUE') + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/test_case.rb b/lib/gitlab/ci/reports/test_case.rb index 75898745366..15a3c862c9e 100644 --- a/lib/gitlab/ci/reports/test_case.rb +++ b/lib/gitlab/ci/reports/test_case.rb @@ -8,7 +8,7 @@ module Gitlab STATUS_FAILED = 'failed' STATUS_SKIPPED = 'skipped' STATUS_ERROR = 'error' - STATUS_TYPES = [STATUS_SUCCESS, STATUS_FAILED, STATUS_SKIPPED, STATUS_ERROR].freeze + STATUS_TYPES = [STATUS_ERROR, STATUS_FAILED, STATUS_SUCCESS, STATUS_SKIPPED].freeze attr_reader :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key, :attachment, :job diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb index 5ee779227ec..e9b78b841e4 100644 --- a/lib/gitlab/ci/reports/test_suite.rb +++ b/lib/gitlab/ci/reports/test_suite.rb @@ -78,11 +78,27 @@ module Gitlab end end + def sorted + sort_by_status + sort_by_execution_time_desc + self + end + private def existing_key?(test_case) @test_cases[test_case.status]&.key?(test_case.key) end + + def sort_by_status + @test_cases = @test_cases.sort_by { |status, _| Gitlab::Ci::Reports::TestCase::STATUS_TYPES.index(status) }.to_h + end + + def sort_by_execution_time_desc + @test_cases = @test_cases.keys.each_with_object({}) do |key, hash| + hash[key] = @test_cases[key].sort_by { |_key, test_case| -test_case.execution_time }.to_h + end + end end end end diff --git a/lib/gitlab/ci/status/bridge/common.rb b/lib/gitlab/ci/status/bridge/common.rb index 4746195c618..b95565b5e09 100644 --- a/lib/gitlab/ci/status/bridge/common.rb +++ b/lib/gitlab/ci/status/bridge/common.rb @@ -10,14 +10,28 @@ module Gitlab end def has_details? - false + !!details_path + end + + def details_path + return unless Feature.enabled?(:ci_bridge_pipeline_details, subject.project, default_enabled: true) + return unless can?(user, :read_pipeline, downstream_pipeline) + + project_pipeline_path(downstream_project, downstream_pipeline) end def has_action? false end - def details_path + private + + def downstream_pipeline + subject.downstream_pipeline + end + + def downstream_project + downstream_pipeline&.project end end end diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb index 88846f724e7..f6562737838 100644 --- a/lib/gitlab/ci/status/build/failed.rb +++ b/lib/gitlab/ci/status/build/failed.rb @@ -25,7 +25,8 @@ module Gitlab insufficient_bridge_permissions: 'no permissions to trigger downstream pipeline', bridge_pipeline_is_child_pipeline: 'creation of child pipeline not allowed from another child pipeline', downstream_pipeline_creation_failed: 'downstream pipeline can not be created', - secrets_provider_not_found: 'secrets provider can not be found' + secrets_provider_not_found: 'secrets provider can not be found', + reached_max_descendant_pipelines_depth: 'reached maximum depth of child pipelines' }.freeze private_constant :REASONS diff --git a/lib/gitlab/ci/status/composite.rb b/lib/gitlab/ci/status/composite.rb index 04a9fc29802..9a4f5644f7d 100644 --- a/lib/gitlab/ci/status/composite.rb +++ b/lib/gitlab/ci/status/composite.rb @@ -7,7 +7,7 @@ module Gitlab include Gitlab::Utils::StrongMemoize # This class accepts an array of arrays/hashes/or objects - def initialize(all_statuses, with_allow_failure: true) + def initialize(all_statuses, with_allow_failure: true, dag: false) unless all_statuses.respond_to?(:pluck) raise ArgumentError, "all_statuses needs to respond to `.pluck`" end @@ -15,6 +15,7 @@ module Gitlab @status_set = Set.new @status_key = 0 @allow_failure_key = 1 if with_allow_failure + @dag = dag consume_all_statuses(all_statuses) end @@ -31,7 +32,13 @@ module Gitlab return if none? strong_memoize(:status) do - if only_of?(:skipped, :ignored) + if @dag && any_of?(:skipped) + # The DAG job is skipped if one of the needs does not run at all. + 'skipped' + elsif @dag && !only_of?(:success, :failed, :canceled, :skipped, :success_with_warnings) + # DAG is blocked from executing if a dependent is not "complete" + 'pending' + elsif only_of?(:skipped, :ignored) 'skipped' elsif only_of?(:success, :skipped, :success_with_warnings, :ignored) 'success' diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 968ff0fce89..6966ce88b30 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -150,16 +150,22 @@ workflow: - exists: - .static +# NOTE: These links point to the latest templates for development in GitLab canonical project, +# therefore the actual templates that were included for Auto DevOps pipelines +# could be different from the contents in the links. +# To view the actual templates, please replace `master` to the specific GitLab version when +# the Auto DevOps pipeline started running e.g. `v13.0.2-ee`. include: - - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml - - template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml - - template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml - - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml - - template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml - - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml - - template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml - - template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml - - template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml - - template: Security/License-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml - - template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml - - template: Security/Secret-Detection.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml + - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml + - template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml + - template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml + - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml + - template: Jobs/Deploy/ECS.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml + - template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml + - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml + - template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml + - template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml + - template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml + - template: Security/License-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml + - template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml + - template: Security/Secret-Detection.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml index 8553a940bd7..5edb26a0b56 100644 --- a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml @@ -7,7 +7,7 @@ performance: variables: DOCKER_TLS_CERTDIR: "" SITESPEED_IMAGE: sitespeedio/sitespeed.io - SITESPEED_VERSION: 13.3.0 + SITESPEED_VERSION: 14.1.0 SITESPEED_OPTIONS: '' services: - docker:19.03.12-dind @@ -20,15 +20,15 @@ performance: fi - export CI_ENVIRONMENT_URL=$(cat environment_url.txt) - mkdir gitlab-exporter - - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.0.1/index.js + - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.1.0/index.js - mkdir sitespeed-results - | if [ -f .gitlab-urls.txt ] then sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt $SITESPEED_OPTIONS + docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results .gitlab-urls.txt $SITESPEED_OPTIONS else - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" $SITESPEED_OPTIONS + docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" $SITESPEED_OPTIONS fi - mv sitespeed-results/data/performance.json browser-performance.json artifacts: 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 cf851c875ee..568ceceeaa2 100644 --- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml @@ -9,6 +9,8 @@ code_quality: DOCKER_TLS_CERTDIR: "" CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.10-gitlab.1" needs: [] + before_script: + - export SOURCE_CODE=$PWD script: - | if ! docker info &>/dev/null; then @@ -16,11 +18,27 @@ code_quality: export DOCKER_HOST='tcp://localhost:2375' fi fi + - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage + function propagate_env_vars() { + CURRENT_ENV=$(printenv) + + for VAR_NAME; do + echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME " + done + } - docker pull --quiet "$CODE_QUALITY_IMAGE" - - docker run - --env SOURCE_CODE="$PWD" - --volume "$PWD":/code - --volume /var/run/docker.sock:/var/run/docker.sock + - | + docker run \ + $(propagate_env_vars \ + SOURCE_CODE \ + TIMEOUT_SECONDS \ + CODECLIMATE_DEBUG \ + CODECLIMATE_DEV \ + REPORT_STDOUT \ + ENGINE_MEMORY_LIMIT_BYTES \ + ) \ + --volume "$PWD":/code \ + --volume /var/run/docker.sock:/var/run/docker.sock \ "$CODE_QUALITY_IMAGE" /code artifacts: reports: diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml index 2922e1c6e88..829fd7a722f 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -2,9 +2,6 @@ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.3" dependencies: [] -include: - - template: Jobs/Deploy/ECS.gitlab-ci.yml - review: extends: .auto-deploy stage: review diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml new file mode 100644 index 00000000000..829fd7a722f --- /dev/null +++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml @@ -0,0 +1,249 @@ +.auto-deploy: + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v1.0.3" + dependencies: [] + +review: + extends: .auto-deploy + stage: review + script: + - auto-deploy check_kube_domain + - auto-deploy download_chart + - auto-deploy ensure_namespace + - auto-deploy initialize_tiller + - auto-deploy create_secret + - auto-deploy deploy + - auto-deploy persist_environment_url + environment: + name: review/$CI_COMMIT_REF_NAME + url: http://$CI_PROJECT_ID-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN + on_stop: stop_review + artifacts: + paths: [environment_url.txt, tiller.log] + when: always + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$CI_COMMIT_BRANCH == "master"' + when: never + - if: '$REVIEW_DISABLED' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' + +stop_review: + extends: .auto-deploy + stage: cleanup + variables: + GIT_STRATEGY: none + script: + - auto-deploy initialize_tiller + - auto-deploy delete + environment: + name: review/$CI_COMMIT_REF_NAME + action: stop + allow_failure: true + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$CI_COMMIT_BRANCH == "master"' + when: never + - if: '$REVIEW_DISABLED' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' + when: manual + +# Staging deploys are disabled by default since +# continuous deployment to production is enabled by default +# If you prefer to automatically deploy to staging and +# only manually promote to production, enable this job by setting +# STAGING_ENABLED. + +staging: + extends: .auto-deploy + stage: staging + script: + - auto-deploy check_kube_domain + - auto-deploy download_chart + - auto-deploy ensure_namespace + - auto-deploy initialize_tiller + - auto-deploy create_secret + - auto-deploy deploy + environment: + name: staging + url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$CI_COMMIT_BRANCH != "master"' + when: never + - if: '$STAGING_ENABLED' + +# Canaries are disabled by default, but if you want them, +# and know what the downsides are, you can enable this by setting +# CANARY_ENABLED. + +canary: + extends: .auto-deploy + stage: canary + allow_failure: true + script: + - auto-deploy check_kube_domain + - auto-deploy download_chart + - auto-deploy ensure_namespace + - auto-deploy initialize_tiller + - auto-deploy create_secret + - auto-deploy deploy canary + environment: + name: production + url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$CI_COMMIT_BRANCH != "master"' + when: never + - if: '$CANARY_ENABLED' + when: manual + +.production: &production_template + extends: .auto-deploy + stage: production + script: + - auto-deploy check_kube_domain + - auto-deploy download_chart + - auto-deploy ensure_namespace + - auto-deploy initialize_tiller + - auto-deploy create_secret + - auto-deploy deploy + - auto-deploy delete canary + - auto-deploy delete rollout + - auto-deploy persist_environment_url + environment: + name: production + url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN + artifacts: + paths: [environment_url.txt, tiller.log] + when: always + +production: + <<: *production_template + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$STAGING_ENABLED' + when: never + - if: '$CANARY_ENABLED' + when: never + - if: '$INCREMENTAL_ROLLOUT_ENABLED' + when: never + - if: '$INCREMENTAL_ROLLOUT_MODE' + when: never + - if: '$CI_COMMIT_BRANCH == "master"' + +production_manual: + <<: *production_template + allow_failure: false + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$INCREMENTAL_ROLLOUT_ENABLED' + when: never + - if: '$INCREMENTAL_ROLLOUT_MODE' + when: never + - if: '$CI_COMMIT_BRANCH == "master" && $STAGING_ENABLED' + when: manual + - if: '$CI_COMMIT_BRANCH == "master" && $CANARY_ENABLED' + when: manual + +# This job implements incremental rollout on for every push to `master`. + +.rollout: &rollout_template + extends: .auto-deploy + script: + - auto-deploy check_kube_domain + - auto-deploy download_chart + - auto-deploy ensure_namespace + - auto-deploy initialize_tiller + - auto-deploy create_secret + - auto-deploy deploy rollout $ROLLOUT_PERCENTAGE + - auto-deploy scale stable $((100-ROLLOUT_PERCENTAGE)) + - auto-deploy delete canary + - auto-deploy persist_environment_url + environment: + name: production + url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN + artifacts: + paths: [environment_url.txt, tiller.log] + when: always + +.manual_rollout_template: &manual_rollout_template + <<: *rollout_template + stage: production + resource_group: production + allow_failure: true + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$INCREMENTAL_ROLLOUT_MODE == "timed"' + when: never + - if: '$CI_COMMIT_BRANCH != "master"' + when: never + # $INCREMENTAL_ROLLOUT_ENABLED is for compamtibilty with pre-GitLab 11.4 syntax + - if: '$INCREMENTAL_ROLLOUT_MODE == "manual" || $INCREMENTAL_ROLLOUT_ENABLED' + when: manual + +.timed_rollout_template: &timed_rollout_template + <<: *rollout_template + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$INCREMENTAL_ROLLOUT_MODE == "manual"' + when: never + - if: '$CI_COMMIT_BRANCH != "master"' + when: never + - if: '$INCREMENTAL_ROLLOUT_MODE == "timed"' + when: delayed + start_in: 5 minutes + +timed rollout 10%: + <<: *timed_rollout_template + stage: incremental rollout 10% + variables: + ROLLOUT_PERCENTAGE: 10 + +timed rollout 25%: + <<: *timed_rollout_template + stage: incremental rollout 25% + variables: + ROLLOUT_PERCENTAGE: 25 + +timed rollout 50%: + <<: *timed_rollout_template + stage: incremental rollout 50% + variables: + ROLLOUT_PERCENTAGE: 50 + +timed rollout 100%: + <<: *timed_rollout_template + <<: *production_template + stage: incremental rollout 100% + variables: + ROLLOUT_PERCENTAGE: 100 + +rollout 10%: + <<: *manual_rollout_template + variables: + ROLLOUT_PERCENTAGE: 10 + +rollout 25%: + <<: *manual_rollout_template + variables: + ROLLOUT_PERCENTAGE: 25 + +rollout 50%: + <<: *manual_rollout_template + variables: + ROLLOUT_PERCENTAGE: 50 + +rollout 100%: + <<: *manual_rollout_template + <<: *production_template + allow_failure: false diff --git a/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml index 4a9849c85c9..9a7c513c25f 100644 --- a/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml @@ -8,6 +8,7 @@ load_performance: K6_VERSION: 0.27.0 K6_TEST_FILE: github.com/loadimpact/k6/samples/http_get.js K6_OPTIONS: '' + K6_DOCKER_OPTIONS: '' services: - docker:19.03.11-dind script: @@ -17,7 +18,7 @@ load_performance: export DOCKER_HOST='tcp://localhost:2375' fi fi - - docker run --rm -v "$(pwd)":/k6 -w /k6 $K6_IMAGE:$K6_VERSION run $K6_TEST_FILE --summary-export=load-performance.json $K6_OPTIONS + - docker run --rm -v "$(pwd)":/k6 -w /k6 $K6_DOCKER_OPTIONS $K6_IMAGE:$K6_VERSION run $K6_TEST_FILE --summary-export=load-performance.json $K6_OPTIONS artifacts: reports: load_performance: load-performance.json 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 3d0bacda853..7050b41e045 100644 --- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml @@ -1,27 +1,11 @@ apply: stage: deploy - image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.24.2" + image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.29.0" environment: name: production variables: TILLER_NAMESPACE: gitlab-managed-apps GITLAB_MANAGED_APPS_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/config.yaml - INGRESS_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/ingress/values.yaml - CERT_MANAGER_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/cert-manager/values.yaml - SENTRY_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/sentry/values.yaml - GITLAB_RUNNER_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/gitlab-runner/values.yaml - CILIUM_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/cilium/values.yaml - CILIUM_HUBBLE_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/cilium/hubble-values.yaml - JUPYTERHUB_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/jupyterhub/values.yaml - PROMETHEUS_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/prometheus/values.yaml - ELASTIC_STACK_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/elastic-stack/values.yaml - VAULT_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/vault/values.yaml - CROSSPLANE_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/crossplane/values.yaml - FLUENTD_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/fluentd/values.yaml - KNATIVE_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/knative/values.yaml - POSTHOG_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/posthog/values.yaml - FALCO_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/falco/values.yaml - APPARMOR_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/apparmor/values.yaml script: - gitlab-managed-apps /usr/local/share/gitlab-managed-apps/helmfile.yaml only: 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 e87f0f28d01..c3a92b67a8b 100644 --- a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml @@ -37,9 +37,6 @@ apifuzzer_fuzz: $FUZZAPI_OPENAPI == null && $FUZZAPI_D_WORKER_IMAGE == null when: never - - if: $FUZZAPI_D_WORKER_IMAGE == null && - $FUZZAPI_TARGET_URL == null - when: never - if: $GITLAB_FEATURES =~ /\bapi_fuzzing\b/ services: - docker:19.03.12-dind @@ -74,13 +71,15 @@ apifuzzer_fuzz: -e FUZZAPI_TIMEOUT \ -e FUZZAPI_VERBOSE \ -e FUZZAPI_SERVICE_START_TIMEOUT \ + -e FUZZAPI_HTTP_USERNAME \ + -e FUZZAPI_HTTP_PASSWORD \ -e GITLAB_FEATURES \ -v $CI_PROJECT_DIR:/app \ -p 80:80 \ -p 8000:8000 \ -p 514:514 \ --restart=no \ - registry.gitlab.com/gitlab-org/security-products/analyzers/api-fuzzing-src:${FUZZAPI_VERSION}-engine + registry.gitlab.com/gitlab-org/security-products/analyzers/api-fuzzing:${FUZZAPI_VERSION}-engine # # Start target container - | @@ -119,6 +118,9 @@ apifuzzer_fuzz: # Wait for testing to complete if api fuzzer is scanning - if [ "$FUZZAPI_HAR$FUZZAPI_OPENAPI" != "" ]; then echo "Waiting for API Fuzzer to exit"; docker wait apifuzzer; fi # + # Propagate exit code from api fuzzer (if any) + - if [[ $(docker inspect apifuzzer --format='{{.State.ExitCode}}') != "0" ]]; then echo "API Fuzzing exited with an error. Logs are available as job artifacts."; docker logs apifuzzer; exit 1; fi + # # Run user provided pre-script - sh -c "$FUZZAPI_POST_SCRIPT" # 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 3f47e575afd..4b957a8f771 100644 --- a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml @@ -34,5 +34,5 @@ variables: rules: - if: $COVFUZZ_DISABLED when: never - - if: $GITLAB_FEATURES =~ /\bcoverage_fuzzing\b/ + - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bcoverage_fuzzing\b/ - if: $CI_RUNNER_EXECUTABLE_ARCH == "linux" 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 d5275c57ef8..3789f0edc1c 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -12,81 +12,24 @@ variables: DS_DEFAULT_ANALYZERS: "bundler-audit, retire.js, gemnasium, gemnasium-maven, gemnasium-python" DS_EXCLUDED_PATHS: "spec, test, tests, tmp" DS_MAJOR_VERSION: 2 - DS_DISABLE_DIND: "true" dependency_scanning: stage: test - image: docker:stable - variables: - DOCKER_DRIVER: overlay2 - DOCKER_TLS_CERTDIR: "" - allow_failure: true - services: - - docker:stable-dind script: - - | - if ! docker info &>/dev/null; then - if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then - export DOCKER_HOST='tcp://localhost:2375' - fi - fi - - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage - function propagate_env_vars() { - CURRENT_ENV=$(printenv) - - for VAR_NAME; do - echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME " - done - } - - | - docker run \ - $(propagate_env_vars \ - DS_ANALYZER_IMAGES \ - SECURE_ANALYZERS_PREFIX \ - DS_ANALYZER_IMAGE_TAG \ - DS_DEFAULT_ANALYZERS \ - DS_EXCLUDED_PATHS \ - DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \ - DS_PULL_ANALYZER_IMAGE_TIMEOUT \ - DS_RUN_ANALYZER_TIMEOUT \ - DS_PYTHON_VERSION \ - DS_PIP_VERSION \ - DS_PIP_DEPENDENCY_PATH \ - DS_JAVA_VERSION \ - GEMNASIUM_DB_LOCAL_PATH \ - GEMNASIUM_DB_REMOTE_URL \ - GEMNASIUM_DB_REF_NAME \ - PIP_INDEX_URL \ - PIP_EXTRA_INDEX_URL \ - PIP_REQUIREMENTS_FILE \ - MAVEN_CLI_OPTS \ - GRADLE_CLI_OPTS \ - SBT_CLI_OPTS \ - BUNDLER_AUDIT_UPDATE_DISABLED \ - BUNDLER_AUDIT_ADVISORY_DB_URL \ - BUNDLER_AUDIT_ADVISORY_DB_REF_NAME \ - RETIREJS_JS_ADVISORY_DB \ - RETIREJS_NODE_ADVISORY_DB \ - DS_REMEDIATE \ - ) \ - --volume "$PWD:/code" \ - --volume /var/run/docker.sock:/var/run/docker.sock \ - "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$DS_MAJOR_VERSION" /code + - echo "$CI_JOB_NAME is used for configuration only, and its script should not be executed" + - exit 1 artifacts: reports: dependency_scanning: gl-dependency-scanning-report.json dependencies: [] rules: - - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'true' - when: never - - if: $CI_COMMIT_BRANCH && - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ + - when: never .ds-analyzer: extends: dependency_scanning - services: [] + allow_failure: true rules: - - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false' + - if: $DEPENDENCY_SCANNING_DISABLED when: never - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bdependency_scanning\b/ @@ -96,9 +39,11 @@ dependency_scanning: gemnasium-dependency_scanning: extends: .ds-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/gemnasium:$DS_MAJOR_VERSION" + name: "$DS_ANALYZER_IMAGE" + variables: + DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gemnasium:$DS_MAJOR_VERSION" rules: - - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false' + - if: $DEPENDENCY_SCANNING_DISABLED when: never - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && @@ -112,13 +57,16 @@ gemnasium-dependency_scanning: - '{package-lock.json,*/package-lock.json,*/*/package-lock.json}' - '{yarn.lock,*/yarn.lock,*/*/yarn.lock}' - '{packages.lock.json,*/packages.lock.json,*/*/packages.lock.json}' + - '{conan.lock,*/conan.lock,*/*/conan.lock}' gemnasium-maven-dependency_scanning: extends: .ds-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/gemnasium-maven:$DS_MAJOR_VERSION" + name: "$DS_ANALYZER_IMAGE" + variables: + DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gemnasium-maven:$DS_MAJOR_VERSION" rules: - - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false' + - if: $DEPENDENCY_SCANNING_DISABLED when: never - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && @@ -132,9 +80,11 @@ gemnasium-maven-dependency_scanning: gemnasium-python-dependency_scanning: extends: .ds-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/gemnasium-python:$DS_MAJOR_VERSION" + name: "$DS_ANALYZER_IMAGE" + variables: + DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gemnasium-python:$DS_MAJOR_VERSION" rules: - - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false' + - if: $DEPENDENCY_SCANNING_DISABLED when: never - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && @@ -155,9 +105,11 @@ gemnasium-python-dependency_scanning: bundler-audit-dependency_scanning: extends: .ds-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/bundler-audit:$DS_MAJOR_VERSION" + name: "$DS_ANALYZER_IMAGE" + variables: + DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/bundler-audit:$DS_MAJOR_VERSION" rules: - - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false' + - if: $DEPENDENCY_SCANNING_DISABLED when: never - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && @@ -168,9 +120,11 @@ bundler-audit-dependency_scanning: retire-js-dependency_scanning: extends: .ds-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/retire.js:$DS_MAJOR_VERSION" + name: "$DS_ANALYZER_IMAGE" + variables: + DS_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/retire.js:$DS_MAJOR_VERSION" rules: - - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false' + - if: $DEPENDENCY_SCANNING_DISABLED when: never - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml index 6eb17341472..77ea11d01d1 100644 --- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -9,48 +9,29 @@ variables: # (SAST, Dependency Scanning, ...) SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers" - SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, secrets, sobelow, pmd-apex, kubesec" + SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, sobelow, pmd-apex, kubesec" SAST_EXCLUDED_PATHS: "spec, test, tests, tmp" SAST_ANALYZER_IMAGE_TAG: 2 - SAST_DISABLE_DIND: "true" SCAN_KUBERNETES_MANIFESTS: "false" sast: stage: test - allow_failure: true artifacts: reports: sast: gl-sast-report.json rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'true' - when: never - - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bsast\b/ - image: docker:stable + - when: never variables: SEARCH_MAX_DEPTH: 4 - DOCKER_DRIVER: overlay2 - DOCKER_TLS_CERTDIR: "" - services: - - docker:stable-dind script: - - | - if ! docker info &>/dev/null; then - if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then - export DOCKER_HOST='tcp://localhost:2375' - fi - fi - - | - docker run \ - $(awk 'BEGIN{for(v in ENVIRON) print v}' | grep -v -E '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | awk '{printf " -e %s", $0}') \ - --volume "$PWD:/code" \ - --volume /var/run/docker.sock:/var/run/docker.sock \ - "registry.gitlab.com/gitlab-org/security-products/sast:$SAST_ANALYZER_IMAGE_TAG" /app/bin/run /code + - echo "$CI_JOB_NAME is used for configuration only, and its script should not be executed" + - exit 1 .sast-analyzer: extends: sast - services: [] + allow_failure: true rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH script: @@ -59,9 +40,11 @@ sast: bandit-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/bandit:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/bandit:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /bandit/ @@ -71,9 +54,11 @@ bandit-sast: brakeman-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /brakeman/ @@ -83,9 +68,11 @@ brakeman-sast: eslint-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /eslint/ @@ -99,9 +86,11 @@ eslint-sast: flawfinder-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/flawfinder:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/flawfinder:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /flawfinder/ @@ -112,9 +101,11 @@ flawfinder-sast: kubesec-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/kubesec:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/kubesec:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /kubesec/ && @@ -123,9 +114,11 @@ kubesec-sast: gosec-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/gosec:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gosec:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /gosec/ @@ -135,9 +128,11 @@ gosec-sast: nodejs-scan-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /nodejs-scan/ @@ -147,9 +142,11 @@ nodejs-scan-sast: phpcs-security-audit-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/phpcs-security-audit:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/phpcs-security-audit:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /phpcs-security-audit/ @@ -159,31 +156,25 @@ phpcs-security-audit-sast: pmd-apex-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/pmd-apex:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/pmd-apex:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /pmd-apex/ exists: - '**/*.cls' -secrets-sast: - extends: .sast-analyzer - image: - name: "$SECURE_ANALYZERS_PREFIX/secrets:$SAST_ANALYZER_IMAGE_TAG" - rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' - when: never - - if: $CI_COMMIT_BRANCH && - $SAST_DEFAULT_ANALYZERS =~ /secrets/ - security-code-scan-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/security-code-scan:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/security-code-scan:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /security-code-scan/ @@ -194,9 +185,11 @@ security-code-scan-sast: sobelow-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/sobelow:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/sobelow:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /sobelow/ @@ -206,9 +199,11 @@ sobelow-sast: spotbugs-sast: extends: .sast-analyzer image: - name: "$SECURE_ANALYZERS_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG" + name: "$SAST_ANALYZER_IMAGE" + variables: + SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG" rules: - - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' + - if: $SAST_DISABLED when: never - if: $CI_COMMIT_BRANCH && $SAST_DEFAULT_ANALYZERS =~ /spotbugs/ 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 b897c7b482f..bde6a0fbebb 100644 --- a/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml @@ -35,6 +35,7 @@ secret_detection: - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH script: - git fetch origin $CI_DEFAULT_BRANCH $CI_BUILD_REF_NAME - - export SECRET_DETECTION_COMMIT_TO=$(git log --left-right --cherry-pick --pretty=format:"%H" refs/remotes/origin/$CI_DEFAULT_BRANCH...refs/remotes/origin/$CI_BUILD_REF_NAME | tail -n 1) - - export SECRET_DETECTION_COMMIT_FROM=$CI_COMMIT_SHA + - git log --left-right --cherry-pick --pretty=format:"%H" refs/remotes/origin/$CI_DEFAULT_BRANCH...refs/remotes/origin/$CI_BUILD_REF_NAME > "$CI_COMMIT_SHA"_commit_list.txt + - export SECRET_DETECTION_COMMITS_FILE="$CI_COMMIT_SHA"_commit_list.txt - /analyzer run + - rm "$CI_COMMIT_SHA"_commit_list.txt diff --git a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml index 9dbd9b679a8..e591e3cc1e2 100644 --- a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml @@ -12,15 +12,15 @@ performance: variables: URL: '' SITESPEED_IMAGE: sitespeedio/sitespeed.io - SITESPEED_VERSION: 13.3.0 + SITESPEED_VERSION: 14.1.0 SITESPEED_OPTIONS: '' services: - docker:stable-dind script: - mkdir gitlab-exporter - - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js + - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.1.0/index.js - mkdir sitespeed-results - - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS + - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --cpu --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS - mv sitespeed-results/data/performance.json browser-performance.json artifacts: paths: diff --git a/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml index f964b3b2caf..cd23af562e5 100644 --- a/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml @@ -14,10 +14,11 @@ load_performance: K6_VERSION: 0.27.0 K6_TEST_FILE: github.com/loadimpact/k6/samples/http_get.js K6_OPTIONS: '' + K6_DOCKER_OPTIONS: '' services: - docker:stable-dind script: - - docker run --rm -v "$(pwd)":/k6 -w /k6 $K6_IMAGE:$K6_VERSION run $K6_TEST_FILE --summary-export=load-performance.json $K6_OPTIONS + - docker run --rm -v "$(pwd)":/k6 -w /k6 $K6_DOCKER_OPTIONS $K6_IMAGE:$K6_VERSION run $K6_TEST_FILE --summary-export=load-performance.json $K6_OPTIONS artifacts: reports: load_performance: load-performance.json diff --git a/lib/gitlab/ci/templates/npm.gitlab-ci.yml b/lib/gitlab/ci/templates/npm.gitlab-ci.yml index 035ba52da84..0a739cf122d 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." + 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 } diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb index f76aacc2d19..348e5472cb4 100644 --- a/lib/gitlab/ci/trace.rb +++ b/lib/gitlab/ci/trace.rb @@ -79,22 +79,13 @@ module Gitlab job.trace_chunks.any? || current_path.present? || old_trace.present? end - def read - stream = Gitlab::Ci::Trace::Stream.new do - if trace_artifact - trace_artifact.open - elsif job.trace_chunks.any? - Gitlab::Ci::Trace::ChunkedIO.new(job) - elsif current_path - File.open(current_path, "rb") - elsif old_trace - StringIO.new(old_trace) - end - end + def read(should_retry: true, &block) + read_stream(&block) + rescue Errno::ENOENT + raise unless should_retry - yield stream - ensure - stream&.close + job.reset + read_stream(&block) end def write(mode, &blk) @@ -141,6 +132,24 @@ module Gitlab private + def read_stream + stream = Gitlab::Ci::Trace::Stream.new do + if trace_artifact + trace_artifact.open + elsif job.trace_chunks.any? + Gitlab::Ci::Trace::ChunkedIO.new(job) + elsif current_path + File.open(current_path, "rb") + elsif old_trace + StringIO.new(old_trace) + end + end + + yield stream + ensure + stream&.close + end + def unsafe_write!(mode, &blk) stream = Gitlab::Ci::Trace::Stream.new do if trace_artifact diff --git a/lib/gitlab/ci/trace/metrics.rb b/lib/gitlab/ci/trace/metrics.rb new file mode 100644 index 00000000000..82a7d5fb83c --- /dev/null +++ b/lib/gitlab/ci/trace/metrics.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Trace + class Metrics + extend Gitlab::Utils::StrongMemoize + + OPERATIONS = [:appended, :streamed, :chunked, :mutated, :overwrite, + :accepted, :finalized, :discarded, :conflict].freeze + + def increment_trace_operation(operation: :unknown) + unless OPERATIONS.include?(operation) + raise ArgumentError, "unknown trace operation: #{operation}" + end + + self.class.trace_operations.increment(operation: operation) + end + + def increment_trace_bytes(size) + self.class.trace_bytes.increment(by: size.to_i) + end + + def self.trace_operations + strong_memoize(:trace_operations) do + name = :gitlab_ci_trace_operations_total + comment = 'Total amount of different operations on a build trace' + + Gitlab::Metrics.counter(name, comment) + end + end + + def self.trace_bytes + strong_memoize(:trace_bytes) do + name = :gitlab_ci_trace_bytes_total + comment = 'Total amount of build trace bytes transferred' + + Gitlab::Metrics.counter(name, comment) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index 20f5620dd64..618438c8887 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -8,7 +8,7 @@ module Gitlab BUFFER_SIZE = 4096 LIMIT_SIZE = 500.kilobytes - attr_reader :stream + attr_reader :stream, :metrics delegate :close, :tell, :seek, :size, :url, :truncate, to: :stream, allow_nil: true @@ -16,9 +16,10 @@ module Gitlab alias_method :present?, :valid? - def initialize + def initialize(metrics = Trace::Metrics.new) @stream = yield @stream&.binmode + @metrics = metrics end def valid? @@ -43,6 +44,9 @@ module Gitlab def append(data, offset) data = data.force_encoding(Encoding::BINARY) + metrics.increment_trace_operation(operation: :streamed) + metrics.increment_trace_bytes(data.bytesize) + stream.seek(offset, IO::SEEK_SET) stream.write(data) stream.truncate(offset + data.bytesize) diff --git a/lib/gitlab/ci/warnings.rb b/lib/gitlab/ci/warnings.rb new file mode 100644 index 00000000000..7138fd21b72 --- /dev/null +++ b/lib/gitlab/ci/warnings.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Gitlab::Ci::Warnings + MAX_LIMIT = 25 +end diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index b7046064f44..ee55eb8b22a 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -1,183 +1,65 @@ # frozen_string_literal: true +# This is the CI Linter component that runs the syntax validations +# while parsing the YAML config into a data structure that is +# then presented to the caller as result object. +# After syntax validations (done by Ci::Config), this component also +# runs logical validation on the built data structure. module Gitlab module Ci class YamlProcessor - # ValidationError is treated like a result object in the form of an exception. - # We can return any warnings, raised during the config validation, along with - # the error object until we support multiple messages to be returned. - class ValidationError < StandardError - attr_reader :warnings - - def initialize(message, warnings: []) - @warnings = warnings - super(message) - end - end - - include Gitlab::Config::Entry::LegacyValidationHelpers + ValidationError = Class.new(StandardError) - attr_reader :stages, :jobs + def self.validation_message(content, opts = {}) + result = new(content, opts).execute - class Result - attr_reader :config, :errors, :warnings + result.errors.first + end - def initialize(config: nil, errors: [], warnings: []) - @config = config - @errors = errors - @warnings = warnings - end + def initialize(config_content, opts = {}) + @config_content = config_content + @opts = opts + end - def valid? - config.present? && errors.empty? + def execute + if @config_content.blank? + return Result.new(errors: ['Please provide content of .gitlab-ci.yml']) end - end - def initialize(config, opts = {}) - @ci_config = Gitlab::Ci::Config.new(config, **opts) - @config = @ci_config.to_hash + @ci_config = Gitlab::Ci::Config.new(@config_content, **@opts) unless @ci_config.valid? - error!(@ci_config.errors.first) + return Result.new(ci_config: @ci_config, errors: @ci_config.errors, warnings: @ci_config.warnings) end - initial_parsing - rescue Gitlab::Ci::Config::ConfigError => e - error!(e.message) - end - - def self.new_with_validation_errors(content, opts = {}) - return Result.new(errors: ['Please provide content of .gitlab-ci.yml']) if content.blank? + run_logical_validations! - config = Gitlab::Ci::Config.new(content, **opts) - return Result.new(errors: config.errors, warnings: config.warnings) unless config.valid? - - config = Gitlab::Ci::YamlProcessor.new(content, opts) - Result.new(config: config, warnings: config.warnings) - - rescue ValidationError => e - Result.new(errors: [e.message], warnings: e.warnings) + Result.new(ci_config: @ci_config, warnings: @ci_config&.warnings) rescue Gitlab::Ci::Config::ConfigError => e - Result.new(errors: [e.message]) - end - - def warnings - @ci_config&.warnings || [] - end - - def builds - @jobs.map do |name, _| - build_attributes(name) - end - end - - def build_attributes(name) - job = @jobs.fetch(name.to_sym, {}) - - { stage_idx: @stages.index(job[:stage]), - stage: job[:stage], - tag_list: job[:tags], - name: job[:name].to_s, - allow_failure: job[:ignore], - when: job[:when] || 'on_success', - environment: job[:environment_name], - coverage_regex: job[:coverage], - yaml_variables: transform_to_yaml_variables(job[:variables]), - needs_attributes: job.dig(:needs, :job), - interruptible: job[:interruptible], - only: job[:only], - except: job[:except], - rules: job[:rules], - cache: job[:cache], - resource_group_key: job[:resource_group], - scheduling_type: job[:scheduling_type], - secrets: job[:secrets], - options: { - image: job[:image], - services: job[:services], - artifacts: job[:artifacts], - dependencies: job[:dependencies], - cross_dependencies: job.dig(:needs, :cross_dependency), - job_timeout: job[:timeout], - before_script: job[:before_script], - script: job[:script], - after_script: job[:after_script], - environment: job[:environment], - retry: job[:retry], - parallel: job[:parallel], - instance: job[:instance], - start_in: job[:start_in], - trigger: job[:trigger], - bridge_needs: job.dig(:needs, :bridge)&.first, - release: release(job) - }.compact }.compact - end + Result.new(ci_config: @ci_config, errors: [e.message], warnings: @ci_config&.warnings) - def release(job) - job[:release] - end - - def stage_builds_attributes(stage) - @jobs.values - .select { |job| job[:stage] == stage } - .map { |job| build_attributes(job[:name]) } - end - - def stages_attributes - @stages.uniq.map do |stage| - seeds = stage_builds_attributes(stage) - - { name: stage, index: @stages.index(stage), builds: seeds } - end - end - - def workflow_attributes - { - rules: @config.dig(:workflow, :rules), - yaml_variables: transform_to_yaml_variables(@variables) - } - end - - def self.validation_message(content, opts = {}) - return 'Please provide content of .gitlab-ci.yml' if content.blank? - - begin - Gitlab::Ci::YamlProcessor.new(content, opts) - nil - rescue ValidationError => e - e.message - end + rescue ValidationError => e + Result.new(ci_config: @ci_config, errors: [e.message], warnings: @ci_config&.warnings) end private - def initial_parsing - ## - # Global config - # - @variables = @ci_config.variables + def run_logical_validations! @stages = @ci_config.stages - - ## - # Jobs - # - @jobs = Ci::Config::Normalizer.new(@ci_config.jobs).normalize_jobs + @jobs = @ci_config.normalized_jobs @jobs.each do |name, job| - # logical validation for job - validate_job_stage!(name, job) - validate_job_dependencies!(name, job) - validate_job_needs!(name, job) - validate_dynamic_child_pipeline_dependencies!(name, job) - validate_job_environment!(name, job) + validate_job!(name, job) end end - def transform_to_yaml_variables(variables) - variables.to_h.map do |key, value| - { key: key.to_s, value: value, public: true } - end + def validate_job!(name, job) + validate_job_stage!(name, job) + validate_job_dependencies!(name, job) + validate_job_needs!(name, job) + validate_dynamic_child_pipeline_dependencies!(name, job) + validate_job_environment!(name, job) end def validate_job_stage!(name, job) @@ -188,10 +70,6 @@ module Gitlab end end - def error!(message) - raise ValidationError.new(message, warnings: warnings) - end - def validate_job_dependencies!(name, job) return unless job[:dependencies] @@ -267,6 +145,10 @@ module Gitlab error!("#{name} job: on_stop job #{on_stop} needs to have action stop defined") end end + + def error!(message) + raise ValidationError.new(message) + end end end end diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb new file mode 100644 index 00000000000..68f61e52df7 --- /dev/null +++ b/lib/gitlab/ci/yaml_processor/result.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +# A data object that wraps `Ci::Config` and any messages +# (errors, warnings) generated by the YamlProcessor. +module Gitlab + module Ci + class YamlProcessor + class Result + attr_reader :errors, :warnings + + def initialize(ci_config: nil, errors: [], warnings: []) + @ci_config = ci_config + @errors = errors || [] + @warnings = warnings || [] + end + + def valid? + errors.empty? + end + + def stages_attributes + stages.uniq.map do |stage| + seeds = stage_builds_attributes(stage) + + { name: stage, index: stages.index(stage), builds: seeds } + end + end + + def builds + jobs.map do |name, _| + build_attributes(name) + end + end + + def stage_builds_attributes(stage) + jobs.values + .select { |job| job[:stage] == stage } + .map { |job| build_attributes(job[:name]) } + end + + def workflow_attributes + { + rules: hash_config.dig(:workflow, :rules), + yaml_variables: transform_to_yaml_variables(variables) + } + end + + def jobs + @jobs ||= @ci_config.normalized_jobs + end + + def stages + @stages ||= @ci_config.stages + end + + def build_attributes(name) + job = jobs.fetch(name.to_sym, {}) + + { stage_idx: stages.index(job[:stage]), + stage: job[:stage], + tag_list: job[:tags], + name: job[:name].to_s, + allow_failure: job[:ignore], + when: job[:when] || 'on_success', + environment: job[:environment_name], + coverage_regex: job[:coverage], + yaml_variables: transform_to_yaml_variables(job[:variables]), + needs_attributes: job.dig(:needs, :job), + interruptible: job[:interruptible], + only: job[:only], + except: job[:except], + rules: job[:rules], + cache: job[:cache], + resource_group_key: job[:resource_group], + scheduling_type: job[:scheduling_type], + secrets: job[:secrets], + options: { + image: job[:image], + services: job[:services], + artifacts: job[:artifacts], + dependencies: job[:dependencies], + cross_dependencies: job.dig(:needs, :cross_dependency), + job_timeout: job[:timeout], + before_script: job[:before_script], + script: job[:script], + after_script: job[:after_script], + environment: job[:environment], + retry: job[:retry], + parallel: job[:parallel], + instance: job[:instance], + start_in: job[:start_in], + trigger: job[:trigger], + bridge_needs: job.dig(:needs, :bridge)&.first, + release: release(job) + }.compact }.compact + end + + private + + def variables + @variables ||= @ci_config.variables + end + + def hash_config + @hash_config ||= @ci_config.to_hash + end + + def release(job) + job[:release] + end + + def transform_to_yaml_variables(variables) + variables.to_h.map do |key, value| + { key: key.to_s, value: value, public: true } + end + end + end + end + end +end diff --git a/lib/gitlab/cleanup/orphan_job_artifact_files.rb b/lib/gitlab/cleanup/orphan_job_artifact_files.rb index 017adc7be4a..6d18f9070cc 100644 --- a/lib/gitlab/cleanup/orphan_job_artifact_files.rb +++ b/lib/gitlab/cleanup/orphan_job_artifact_files.rb @@ -18,7 +18,7 @@ module Gitlab @limit = limit @dry_run = dry_run @niceness = (niceness || DEFAULT_NICENESS).downcase - @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger + @logger = logger || Gitlab::AppLogger @total_found = @total_cleaned = 0 new_batch! diff --git a/lib/gitlab/cleanup/orphan_job_artifact_files_batch.rb b/lib/gitlab/cleanup/orphan_job_artifact_files_batch.rb index 6ad05c7b2e4..4b1d16eb974 100644 --- a/lib/gitlab/cleanup/orphan_job_artifact_files_batch.rb +++ b/lib/gitlab/cleanup/orphan_job_artifact_files_batch.rb @@ -22,7 +22,7 @@ module Gitlab attr_reader :batch_size, :dry_run attr_accessor :artifact_files - def initialize(batch_size:, dry_run: true, logger: Rails.logger) # rubocop:disable Gitlab/RailsLogger + def initialize(batch_size:, dry_run: true, logger: Gitlab::AppLogger) @batch_size = batch_size @dry_run = dry_run @logger = logger diff --git a/lib/gitlab/cleanup/orphan_lfs_file_references.rb b/lib/gitlab/cleanup/orphan_lfs_file_references.rb index 3df243e319e..14eac474e27 100644 --- a/lib/gitlab/cleanup/orphan_lfs_file_references.rb +++ b/lib/gitlab/cleanup/orphan_lfs_file_references.rb @@ -12,7 +12,7 @@ module Gitlab def initialize(project, dry_run: true, logger: nil, limit: nil) @project = project @dry_run = dry_run - @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger + @logger = logger || Gitlab::AppLogger @limit = limit end @@ -25,7 +25,7 @@ module Gitlab private def remove_orphan_references - invalid_references = project.lfs_objects_projects.where(lfs_object: orphan_objects) # rubocop:disable CodeReuse/ActiveRecord + invalid_references = project.lfs_objects_projects.lfs_object_in(orphan_objects) if dry_run log_info("Found invalid references: #{invalid_references.count}") @@ -41,26 +41,22 @@ module Gitlab end end - def lfs_oids_from_repository - project.repository.gitaly_blob_client.get_all_lfs_pointers.map(&:lfs_oid) - end - - def orphan_oids - lfs_oids_from_database - lfs_oids_from_repository - end + def orphan_objects + # Get these first so racing with a git push can't remove any LFS objects + oids = project.lfs_objects_oids - def lfs_oids_from_database - oids = [] + repos = [ + project.repository, + project.design_repository, + project.wiki.repository + ].select(&:exists?) - project.lfs_objects.each_batch do |relation| - oids += relation.pluck(:oid) # rubocop:disable CodeReuse/ActiveRecord + repos.flat_map do |repo| + oids -= repo.gitaly_blob_client.get_all_lfs_pointers.map(&:lfs_oid) end - oids - end - - def orphan_objects - LfsObject.where(oid: orphan_oids) # rubocop:disable CodeReuse/ActiveRecord + # The remaining OIDs are not used by any repository, so are orphans + LfsObject.for_oids(oids) end def log_info(msg) diff --git a/lib/gitlab/cleanup/project_upload_file_finder.rb b/lib/gitlab/cleanup/project_upload_file_finder.rb index 3d35d474f5d..0f40f683354 100644 --- a/lib/gitlab/cleanup/project_upload_file_finder.rb +++ b/lib/gitlab/cleanup/project_upload_file_finder.rb @@ -49,7 +49,7 @@ module Gitlab cmd = %W[#{ionice} -c Idle] + cmd if ionice log_msg = "find command: \"#{cmd.join(' ')}\"" - Rails.logger.info log_msg # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.info log_msg cmd end diff --git a/lib/gitlab/cleanup/project_uploads.rb b/lib/gitlab/cleanup/project_uploads.rb index 056e075cb21..77231665e7e 100644 --- a/lib/gitlab/cleanup/project_uploads.rb +++ b/lib/gitlab/cleanup/project_uploads.rb @@ -8,7 +8,7 @@ module Gitlab attr_reader :logger def initialize(logger: nil) - @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger + @logger = logger || Gitlab::AppLogger end def run!(dry_run: true) diff --git a/lib/gitlab/cleanup/remote_uploads.rb b/lib/gitlab/cleanup/remote_uploads.rb index 42c93b7aecb..6cadb9424f7 100644 --- a/lib/gitlab/cleanup/remote_uploads.rb +++ b/lib/gitlab/cleanup/remote_uploads.rb @@ -7,7 +7,7 @@ module Gitlab BATCH_SIZE = 100 def initialize(logger: nil) - @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger + @logger = logger || Gitlab::AppLogger end def run!(dry_run: false) diff --git a/lib/gitlab/consul/internal.rb b/lib/gitlab/consul/internal.rb new file mode 100644 index 00000000000..3afc24ddab9 --- /dev/null +++ b/lib/gitlab/consul/internal.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Gitlab + module Consul + class Internal + Error = Class.new(StandardError) + UnexpectedResponseError = Class.new(Gitlab::Consul::Internal::Error) + SocketError = Class.new(Gitlab::Consul::Internal::Error) + SSLError = Class.new(Gitlab::Consul::Internal::Error) + ECONNREFUSED = Class.new(Gitlab::Consul::Internal::Error) + + class << self + def api_url + Gitlab.config.consul.api_url.to_s.presence if Gitlab.config.consul + rescue Settingslogic::MissingSetting + Gitlab::AppLogger.error('Consul api_url is not present in config/gitlab.yml') + + nil + end + + def discover_service(service_name:) + return unless service_name.present? && api_url + + api_path = URI.join(api_url, '/v1/catalog/service/', URI.encode_www_form_component(service_name)).to_s + services = json_get(api_path, allow_local_requests: true, open_timeout: 5, read_timeout: 10) + + # Use the first service definition + service = services&.first + + return unless service + + service_address = service['ServiceAddress'] || service['Address'] + service_port = service['ServicePort'] + + [service_address, service_port] + end + + def discover_prometheus_server_address + service_address, service_port = discover_service(service_name: 'prometheus') + + return unless service_address && service_port + + "#{service_address}:#{service_port}" + end + + private + + def json_get(path, options) + response = get(path, options) + code = response.try(:code) + body = response.try(:body) + + raise Consul::Internal::UnexpectedResponseError unless code == 200 && body + + parse_response_body(body) + end + + def parse_response_body(body) + Gitlab::Json.parse(body) + rescue + raise Consul::Internal::UnexpectedResponseError + end + + def get(path, options) + Gitlab::HTTP.get(path, options) + rescue ::SocketError + raise Consul::Internal::SocketError + rescue OpenSSL::SSL::SSLError + raise Consul::Internal::SSLError + rescue Errno::ECONNREFUSED + raise Consul::Internal::ECONNREFUSED + rescue + raise Consul::Internal::UnexpectedResponseError + end + end + end + end +end diff --git a/lib/gitlab/cross_project_access.rb b/lib/gitlab/cross_project_access.rb index 4ddc7e02d1b..93baf1e596c 100644 --- a/lib/gitlab/cross_project_access.rb +++ b/lib/gitlab/cross_project_access.rb @@ -18,7 +18,7 @@ module Gitlab end def add_check( - klass, + klass, actions: {}, positive_condition: nil, negative_condition: nil, diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb deleted file mode 100644 index d5f2e868606..00000000000 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module CycleAnalytics - class ProductionStage < BaseStage - include ProductionHelper - - def start_time_attrs - @start_time_attrs ||= issue_table[:created_at] - end - - def end_time_attrs - @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at] - end - - def name - :production - end - - def title - s_('CycleAnalyticsStage|Total') - end - - def legend - _("Related Issues") - end - - def description - _("From issue creation until deploy to production") - end - - def query - # Limit to merge requests that have been deployed to production after `@from` - query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from)) - end - end - end -end diff --git a/lib/gitlab/danger/changelog.rb b/lib/gitlab/danger/changelog.rb index 4427c331b8e..607ca1200a0 100644 --- a/lib/gitlab/danger/changelog.rb +++ b/lib/gitlab/danger/changelog.rb @@ -11,8 +11,36 @@ module Gitlab 'meta' ].freeze NO_CHANGELOG_CATEGORIES = %i[docs none].freeze + CREATE_CHANGELOG_COMMAND = 'bin/changelog -m %<mr_iid>s "%<mr_title>s"' + CREATE_EE_CHANGELOG_COMMAND = 'bin/changelog --ee -m %<mr_iid>s "%<mr_title>s"' + CHANGELOG_MODIFIED_URL_TEXT = "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n" + CHANGELOG_MISSING_URL_TEXT = "**[CHANGELOG missing](https://docs.gitlab.com/ee/development/changelog.html)**:\n\n" - def needed? + OPTIONAL_CHANGELOG_MESSAGE = <<~MSG + If you want to create a changelog entry for GitLab FOSS, run the following: + + #{CREATE_CHANGELOG_COMMAND} + + If you want to create a changelog entry for GitLab EE, run the following instead: + + #{CREATE_EE_CHANGELOG_COMMAND} + + If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message. + MSG + + REQUIRED_CHANGELOG_MESSAGE = <<~MSG + To create a changelog entry, run the following: + + #{CREATE_CHANGELOG_COMMAND} + + This merge request requires a changelog entry because it [introduces a database migration](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry). + MSG + + def required? + git.added_files.any? { |path| path =~ %r{\Adb/(migrate|post_migrate)/} } + end + + def optional? categories_need_changelog? && without_no_changelog_label? end @@ -20,16 +48,35 @@ module Gitlab @found ||= git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} } end - def sanitized_mr_title - gitlab.mr_json["title"].gsub(/^WIP: */, '').gsub(/`/, '\\\`') - end - def ee_changelog? found.start_with?('ee/') end + def modified_text + CHANGELOG_MODIFIED_URL_TEXT + + format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title) + end + + def required_text + CHANGELOG_MISSING_URL_TEXT + + format(REQUIRED_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title) + end + + def optional_text + CHANGELOG_MISSING_URL_TEXT + + format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title) + end + private + def mr_iid + gitlab.mr_json["iid"] + end + + def sanitized_mr_title + helper.sanitize_mr_title(gitlab.mr_json["title"]) + end + def categories_need_changelog? (helper.changes_by_category.keys - NO_CHANGELOG_CATEGORIES).any? end diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb index 077c71f1233..3626ec5bf5b 100644 --- a/lib/gitlab/danger/helper.rb +++ b/lib/gitlab/danger/helper.rb @@ -44,7 +44,10 @@ module Gitlab # "+ # Test change", # "- # Old change" ] def changed_lines(changed_file) - git.diff_for_file(changed_file).patch.split("\n").select { |line| %r{^[+-]}.match?(line) } + diff = git.diff_for_file(changed_file) + return [] unless diff + + diff.patch.split("\n").select { |line| %r{^[+-]}.match?(line) } end def all_ee_changes @@ -171,6 +174,7 @@ module Gitlab %r{\A(ee/)?scripts/} => :engineering_productivity, %r{\Atooling/} => :engineering_productivity, %r{(CODEOWNERS)} => :engineering_productivity, + %r{(tests.yml)} => :engineering_productivity, %r{\A(ee/)?spec/features/} => :test, %r{\A(ee/)?spec/support/shared_examples/features/} => :test, @@ -191,6 +195,7 @@ module Gitlab # Files that don't fit into any category are marked with :none %r{\A(ee/)?changelogs/} => :none, %r{\Alocale/gitlab\.pot\z} => :none, + %r{\Adata/whats_new/} => :none, # Fallbacks in case the above patterns miss anything %r{\.rb\z} => :backend, @@ -205,16 +210,6 @@ module Gitlab usernames.map { |u| Gitlab::Danger::Teammate.new('username' => u) } end - def missing_database_labels(current_mr_labels) - labels = if has_database_scoped_labels?(current_mr_labels) - ['database'] - else - ['database', 'database::review pending'] - end - - labels - current_mr_labels - end - def sanitize_mr_title(title) title.gsub(DRAFT_REGEX, '').gsub(/`/, '\\\`') end @@ -258,8 +253,6 @@ module Gitlab all_changed_files.grep(regex) end - private - def has_database_scoped_labels?(current_mr_labels) current_mr_labels.any? { |label| label.start_with?('database::') } end diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb index 2e6181d1cab..a6866868e6c 100644 --- a/lib/gitlab/danger/roulette.rb +++ b/lib/gitlab/danger/roulette.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative 'teammate' -require_relative 'request_helper' +require_relative 'request_helper' unless defined?(Gitlab::Danger::RequestHelper) module Gitlab module Danger diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb index 9b389907090..ebd96be40d7 100644 --- a/lib/gitlab/danger/teammate.rb +++ b/lib/gitlab/danger/teammate.rb @@ -45,9 +45,7 @@ module Gitlab has_capability?(project, category, :maintainer, labels) end - def markdown_name(timezone_experiment: false, author: nil) - return @markdown_name unless timezone_experiment - + def markdown_name(author: nil) "#{@markdown_name} (#{utc_offset_text(author)})" end diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index af363705bed..f941c57a6dd 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -86,7 +86,7 @@ module Gitlab # # rubocop:disable Metrics/ParameterLists def build( - project:, user:, ref:, oldrev: nil, newrev: nil, + project:, user:, ref:, oldrev: nil, newrev: nil, commits: [], commits_count: nil, message: nil, push_options: {}, with_changed_files: true) diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 990c940d200..accc6330253 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -335,7 +335,7 @@ module Gitlab end rescue Prometheus::Client::LabelSetValidator::LabelSetError => err # Ensure that errors in recording these metrics don't affect the operation of the application - Rails.logger.error("Unable to observe database transaction duration: #{err}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error("Unable to observe database transaction duration: #{err}") end # MonkeyPatch for ActiveRecord::Base for adding observability diff --git a/lib/gitlab/database/background_migration_job.rb b/lib/gitlab/database/background_migration_job.rb index 445735b232a..1b9d7cbc9a1 100644 --- a/lib/gitlab/database/background_migration_job.rb +++ b/lib/gitlab/database/background_migration_job.rb @@ -3,6 +3,8 @@ module Gitlab module Database class BackgroundMigrationJob < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord + include EachBatch + self.table_name = :background_migration_jobs scope :for_migration_class, -> (class_name) { where(class_name: normalize_class_name(class_name)) } diff --git a/lib/gitlab/database/concurrent_reindex.rb b/lib/gitlab/database/concurrent_reindex.rb new file mode 100644 index 00000000000..485ab35e55d --- /dev/null +++ b/lib/gitlab/database/concurrent_reindex.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +module Gitlab + module Database + class ConcurrentReindex + include Gitlab::Utils::StrongMemoize + include MigrationHelpers + + ReindexError = Class.new(StandardError) + + PG_IDENTIFIER_LENGTH = 63 + TEMPORARY_INDEX_PREFIX = 'tmp_reindex_' + REPLACED_INDEX_PREFIX = 'old_reindex_' + + attr_reader :index_name, :logger + + def initialize(index_name, logger:) + @index_name = index_name + @logger = logger + end + + def execute + raise ReindexError, "index #{index_name} does not exist" unless index_exists? + + raise ReindexError, 'UNIQUE indexes are currently not supported' if index_unique? + + logger.debug("dropping dangling index from previous run: #{replacement_index_name}") + remove_replacement_index + + begin + create_replacement_index + + unless replacement_index_valid? + message = 'replacement index was created as INVALID' + logger.error("#{message}, cleaning up") + raise ReindexError, "failed to reindex #{index_name}: #{message}" + end + + swap_replacement_index + rescue Gitlab::Database::WithLockRetries::AttemptsExhaustedError => e + logger.error('failed to obtain the required database locks to swap the indexes, cleaning up') + raise ReindexError, e.message + rescue ActiveRecord::ActiveRecordError, PG::Error => e + logger.error("database error while attempting reindex of #{index_name}: #{e.message}") + raise ReindexError, e.message + ensure + logger.info("dropping unneeded replacement index: #{replacement_index_name}") + remove_replacement_index + end + end + + private + + def connection + @connection ||= ActiveRecord::Base.connection + end + + def replacement_index_name + @replacement_index_name ||= constrained_index_name(TEMPORARY_INDEX_PREFIX) + end + + def index + strong_memoize(:index) do + find_index(index_name) + end + end + + def index_exists? + !index.nil? + end + + def index_unique? + index.indisunique + end + + def constrained_index_name(prefix) + "#{prefix}#{index_name}".slice(0, PG_IDENTIFIER_LENGTH) + end + + def create_replacement_index + create_replacement_index_statement = index.indexdef + .sub(/CREATE INDEX/, 'CREATE INDEX CONCURRENTLY') + .sub(/#{index_name}/, replacement_index_name) + + logger.info("creating replacement index #{replacement_index_name}") + logger.debug("replacement index definition: #{create_replacement_index_statement}") + + disable_statement_timeout do + connection.execute(create_replacement_index_statement) + end + end + + def replacement_index_valid? + find_index(replacement_index_name).indisvalid + end + + def find_index(index_name) + record = connection.select_one(<<~SQL) + SELECT + pg_index.indisunique, + pg_index.indisvalid, + pg_indexes.indexdef + FROM pg_index + INNER JOIN pg_class ON pg_class.oid = pg_index.indexrelid + INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid + INNER JOIN pg_indexes ON pg_class.relname = pg_indexes.indexname + WHERE pg_namespace.nspname = 'public' + AND pg_class.relname = #{connection.quote(index_name)} + SQL + + OpenStruct.new(record) if record + end + + def swap_replacement_index + replaced_index_name = constrained_index_name(REPLACED_INDEX_PREFIX) + + logger.info("swapping replacement index #{replacement_index_name} with #{index_name}") + + with_lock_retries do + rename_index(index_name, replaced_index_name) + rename_index(replacement_index_name, index_name) + rename_index(replaced_index_name, replacement_index_name) + end + end + + def rename_index(old_index_name, new_index_name) + connection.execute("ALTER INDEX #{old_index_name} RENAME TO #{new_index_name}") + end + + def remove_replacement_index + disable_statement_timeout do + connection.execute("DROP INDEX CONCURRENTLY IF EXISTS #{replacement_index_name}") + end + end + + def with_lock_retries(&block) + arguments = { klass: self.class, logger: logger } + + Gitlab::Database::WithLockRetries.new(arguments).run(raise_on_exhaustion: true, &block) + end + end + end +end diff --git a/lib/gitlab/database/custom_structure.rb b/lib/gitlab/database/custom_structure.rb index c5a76c5a787..e4404e73a63 100644 --- a/lib/gitlab/database/custom_structure.rb +++ b/lib/gitlab/database/custom_structure.rb @@ -8,8 +8,7 @@ module Gitlab def dump File.open(self.class.custom_dump_filepath, 'wb') do |io| io << "-- this file tracks custom GitLab data, such as foreign keys referencing partitioned tables\n" - io << "-- more details can be found in the issue: https://gitlab.com/gitlab-org/gitlab/-/issues/201872\n" - io << "SET search_path=public;\n\n" + io << "-- more details can be found in the issue: https://gitlab.com/gitlab-org/gitlab/-/issues/201872\n\n" dump_partitioned_foreign_keys(io) if partitioned_foreign_keys_exist? end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index b62b6e20dd5..723f0f6a308 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -87,7 +87,7 @@ module Gitlab options = options.merge({ algorithm: :concurrently }) if index_exists?(table_name, column_name, options) - Rails.logger.warn "Index not created because it already exists (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn "Index not created because it already exists (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}" return end @@ -113,7 +113,7 @@ module Gitlab options = options.merge({ algorithm: :concurrently }) unless index_exists?(table_name, column_name, options) - Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}" return end @@ -143,7 +143,7 @@ module Gitlab options = options.merge({ algorithm: :concurrently }) unless index_exists_by_name?(table_name, index_name) - Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, index_name: #{index_name}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, index_name: #{index_name}" return end @@ -163,7 +163,6 @@ module Gitlab # defaults to "CASCADE". # name - The name of the foreign key. # - # rubocop:disable Gitlab/RailsLogger def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade, name: nil, validate: true) # Transactions would result in ALTER TABLE locks being held for the # duration of the transaction, defeating the purpose of this method. @@ -183,7 +182,7 @@ module Gitlab "source: #{source}, target: #{target}, column: #{options[:column]}, "\ "name: #{options[:name]}, on_delete: #{options[:on_delete]}" - Rails.logger.warn warning_message + Gitlab::AppLogger.warn warning_message else # Using NOT VALID allows us to create a key without immediately # validating it. This means we keep the ALTER TABLE lock only for a @@ -217,7 +216,6 @@ module Gitlab end end end - # rubocop:enable Gitlab/RailsLogger def validate_foreign_key(source, column, name: nil) fk_name = name || concurrent_foreign_key_name(source, column) @@ -540,10 +538,10 @@ module Gitlab # table - The table containing the column. # column - The name of the column to change. # new_type - The new column type. - def change_column_type_concurrently(table, column, new_type, type_cast_function: nil) + def change_column_type_concurrently(table, column, new_type, type_cast_function: nil, batch_column_name: :id) temp_column = "#{column}_for_type_change" - rename_column_concurrently(table, column, temp_column, type: new_type, type_cast_function: type_cast_function) + rename_column_concurrently(table, column, temp_column, type: new_type, type_cast_function: type_cast_function, batch_column_name: batch_column_name) end # Performs cleanup of a concurrent type change. @@ -1085,7 +1083,6 @@ into similar problems in the future (e.g. when new tables are created). # Should be unique per table (not per column) # validate - Whether to validate the constraint in this call # - # rubocop:disable Gitlab/RailsLogger def add_check_constraint(table, check, constraint_name, validate: true) validate_check_constraint_name!(constraint_name) @@ -1102,7 +1099,7 @@ into similar problems in the future (e.g. when new tables are created). table: #{table}, check: #{check}, constraint name: #{constraint_name} MESSAGE - Rails.logger.warn warning_message + Gitlab::AppLogger.warn warning_message else # Only add the constraint without validating it # Even though it is fast, ADD CONSTRAINT requires an EXCLUSIVE lock @@ -1187,7 +1184,7 @@ into similar problems in the future (e.g. when new tables are created). column #{table}.#{column} is already defined as `NOT NULL` MESSAGE - Rails.logger.warn warning_message + Gitlab::AppLogger.warn warning_message end end diff --git a/lib/gitlab/database/partitioning/partition_monitoring.rb b/lib/gitlab/database/partitioning/partition_monitoring.rb new file mode 100644 index 00000000000..9ec9ae684a5 --- /dev/null +++ b/lib/gitlab/database/partitioning/partition_monitoring.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Gitlab + module Database + module Partitioning + class PartitionMonitoring + attr_reader :models + + def initialize(models = PartitionCreator.models) + @models = models + end + + def report_metrics + models.each do |model| + strategy = model.partitioning_strategy + + gauge_present.set({ table: model.table_name }, strategy.current_partitions.size) + gauge_missing.set({ table: model.table_name }, strategy.missing_partitions.size) + end + end + + private + + def gauge_present + @gauge_present ||= Gitlab::Metrics.gauge(:db_partitions_present, 'Number of database partitions present') + end + + def gauge_missing + @gauge_missing ||= Gitlab::Metrics.gauge(:db_partitions_missing, 'Number of database partitions currently expected, but not present') + end + end + end + end +end diff --git a/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb index 1fb9476b7d9..2def3a4d3a9 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb @@ -31,7 +31,7 @@ module Gitlab current_keys << specified_key else - Rails.logger.warn "foreign key not added because it already exists: #{specified_key}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn "foreign key not added because it already exists: #{specified_key}" current_keys end end @@ -56,7 +56,7 @@ module Gitlab existing_key.delete current_keys.delete(existing_key) else - Rails.logger.warn "foreign key not removed because it doesn't exist: #{specified_key}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn "foreign key not removed because it doesn't exist: #{specified_key}" end current_keys diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb index 84b6fb9f76e..f7b0306b769 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb @@ -6,6 +6,7 @@ module Gitlab module TableManagementHelpers include ::Gitlab::Database::SchemaHelpers include ::Gitlab::Database::DynamicModelHelpers + include ::Gitlab::Database::MigrationHelpers include ::Gitlab::Database::Migrations::BackgroundMigrationHelpers ALLOWED_TABLES = %w[audit_events].freeze @@ -15,6 +16,12 @@ module Gitlab BATCH_INTERVAL = 2.minutes.freeze BATCH_SIZE = 50_000 + JobArguments = Struct.new(:start_id, :stop_id, :source_table_name, :partitioned_table_name, :source_column) do + def self.from_array(arguments) + self.new(*arguments) + end + end + # Creates a partitioned copy of an existing table, using a RANGE partitioning strategy on a timestamp column. # One partition is created per month between the given `min_date` and `max_date`. Also installs a trigger on # the original table to copy writes into the partitioned table. To copy over historic data from before creation @@ -134,6 +141,42 @@ module Gitlab end end + # Executes cleanup tasks from a previous BackgroundMigration to backfill a partitioned table by finishing + # pending jobs and performing a final data synchronization. + # This performs two steps: + # 1. Wait to finish any pending BackgroundMigration jobs that have not succeeded + # 2. Inline copy any missed rows from the original table to the partitioned table + # + # **NOTE** Migrations using this method cannot be scheduled in the same release as the migration that + # schedules the background migration using the `enqueue_background_migration` helper, or else the + # background migration jobs will be force-executed. + # + # Example: + # + # finalize_backfilling_partitioned_table :audit_events + # + def finalize_backfilling_partitioned_table(table_name) + assert_table_is_allowed(table_name) + assert_not_in_transaction_block(scope: ERROR_SCOPE) + + partitioned_table_name = make_partitioned_table_name(table_name) + unless table_exists?(partitioned_table_name) + raise "could not find partitioned table for #{table_name}, " \ + "this could indicate the previous partitioning migration has been rolled back." + end + + Gitlab::BackgroundMigration.steal(MIGRATION_CLASS_NAME) do |raw_arguments| + JobArguments.from_array(raw_arguments).source_table_name == table_name.to_s + end + + primary_key = connection.primary_key(table_name) + copy_missed_records(table_name, partitioned_table_name, primary_key) + + disable_statement_timeout do + execute("VACUUM FREEZE ANALYZE #{partitioned_table_name}") + end + end + private def assert_table_is_allowed(table_name) @@ -161,10 +204,8 @@ module Gitlab def create_range_partitioned_copy(source_table_name, partitioned_table_name, partition_column, primary_key) if table_exists?(partitioned_table_name) - # rubocop:disable Gitlab/RailsLogger - Rails.logger.warn "Partitioned table not created because it already exists" \ + Gitlab::AppLogger.warn "Partitioned table not created because it already exists" \ " (this may be due to an aborted migration or similar): table_name: #{partitioned_table_name} " - # rubocop:enable Gitlab/RailsLogger return end @@ -217,10 +258,8 @@ module Gitlab def create_range_partition_safely(partition_name, table_name, lower_bound, upper_bound) if table_exists?(table_for_range_partition(partition_name)) - # rubocop:disable Gitlab/RailsLogger - Rails.logger.warn "Partition not created because it already exists" \ + Gitlab::AppLogger.warn "Partition not created because it already exists" \ " (this may be due to an aborted migration or similar): partition_name: #{partition_name}" - # rubocop:enable Gitlab/RailsLogger return end @@ -241,10 +280,8 @@ module Gitlab def create_sync_function(name, partitioned_table_name, unique_key) if function_exists?(name) - # rubocop:disable Gitlab/RailsLogger - Rails.logger.warn "Partitioning sync function not created because it already exists" \ + Gitlab::AppLogger.warn "Partitioning sync function not created because it already exists" \ " (this may be due to an aborted migration or similar): function name: #{name}" - # rubocop:enable Gitlab/RailsLogger return end @@ -276,17 +313,15 @@ module Gitlab def create_sync_trigger(table_name, trigger_name, function_name) if trigger_exists?(table_name, trigger_name) - # rubocop:disable Gitlab/RailsLogger - Rails.logger.warn "Partitioning sync trigger not created because it already exists" \ + Gitlab::AppLogger.warn "Partitioning sync trigger not created because it already exists" \ " (this may be due to an aborted migration or similar): trigger name: #{trigger_name}" - # rubocop:enable Gitlab/RailsLogger return end create_trigger(table_name, trigger_name, function_name, fires: 'AFTER INSERT OR UPDATE OR DELETE') end - def enqueue_background_migration(source_table_name, partitioned_table_name, source_key) + def enqueue_background_migration(source_table_name, partitioned_table_name, source_column) source_model = define_batchable_model(source_table_name) queue_background_migration_jobs_by_range_at_intervals( @@ -294,13 +329,35 @@ module Gitlab MIGRATION_CLASS_NAME, BATCH_INTERVAL, batch_size: BATCH_SIZE, - other_job_arguments: [source_table_name.to_s, partitioned_table_name, source_key], + other_job_arguments: [source_table_name.to_s, partitioned_table_name, source_column], track_jobs: true) end def cleanup_migration_jobs(table_name) ::Gitlab::Database::BackgroundMigrationJob.for_partitioning_migration(MIGRATION_CLASS_NAME, table_name).delete_all end + + def copy_missed_records(source_table_name, partitioned_table_name, source_column) + backfill_table = BackfillPartitionedTable.new + relation = ::Gitlab::Database::BackgroundMigrationJob.pending + .for_partitioning_migration(MIGRATION_CLASS_NAME, source_table_name) + + relation.each_batch do |batch| + batch.each do |pending_migration_job| + job_arguments = JobArguments.from_array(pending_migration_job.arguments) + start_id = job_arguments.start_id + stop_id = job_arguments.stop_id + + say("Backfilling data into partitioned table for ids from #{start_id} to #{stop_id}") + job_updated_count = backfill_table.perform(start_id, stop_id, source_table_name, + partitioned_table_name, source_column) + + unless job_updated_count > 0 + raise "failed to update tracking record for ids from #{start_id} to #{stop_id}" + end + end + end + end end end end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb index 4fbbfdc4914..562e651cabc 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb @@ -71,7 +71,7 @@ module Gitlab unless gitlab_shell.mv_namespace(repository_storage, old_full_path, new_full_path) message = "Exception moving on shard #{repository_storage} from #{old_full_path} to #{new_full_path}" - Rails.logger.error message # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error message end end end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb index 8b92b296408..5dbf30bad4e 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb @@ -56,7 +56,7 @@ module Gitlab unless gitlab_shell.mv_repository(project.repository_storage, old_path, new_path) - Rails.logger.error "Error moving #{old_path} to #{new_path}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error "Error moving #{old_path} to #{new_path}" end end diff --git a/lib/gitlab/database/schema_cleaner.rb b/lib/gitlab/database/schema_cleaner.rb index 7c415287878..8f93da2b66c 100644 --- a/lib/gitlab/database/schema_cleaner.rb +++ b/lib/gitlab/database/schema_cleaner.rb @@ -18,11 +18,18 @@ module Gitlab structure.gsub!(/^SELECT pg_catalog\.set_config\('search_path'.+/, '') structure.gsub!(/^--.*/, "\n") - structure = "SET search_path=public;\n" + structure + # We typically don't assume we're working with the public schema. + # pg_dump uses fully qualified object names though, since we have multiple schemas + # in the database. + # + # The intention here is to not introduce an assumption about the standard schema, + # unless we have a good reason to do so. + structure.gsub!(/public\.(\w+)/, '\1') + structure.gsub!(/CREATE EXTENSION IF NOT EXISTS (\w+) WITH SCHEMA public;/, 'CREATE EXTENSION IF NOT EXISTS \1;') structure.gsub!(/\n{3,}/, "\n\n") - io << structure + io << structure.strip io << <<~MSG -- schema_migrations.version information is no longer stored in this file, -- but instead tracked in the db/schema_migrations directory diff --git a/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb b/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb index fb0fcc5a93b..8a5f53be20f 100644 --- a/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb +++ b/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb @@ -18,6 +18,7 @@ module Gitlab business: 0, response: 1, system: 2, + custom: 3, cluster_health: -100 } @@ -34,7 +35,8 @@ module Gitlab aws_elb: _('Response metrics (AWS ELB)'), nginx: _('Response metrics (NGINX)'), kubernetes: _('System metrics (Kubernetes)'), - cluster_health: _('Cluster Health') + cluster_health: _('Cluster Health'), + custom: _('Custom metrics') } end end diff --git a/lib/gitlab/database_importers/instance_administrators/create_group.rb b/lib/gitlab/database_importers/instance_administrators/create_group.rb index 5bf0e5a320d..d9425810405 100644 --- a/lib/gitlab/database_importers/instance_administrators/create_group.rb +++ b/lib/gitlab/database_importers/instance_administrators/create_group.rb @@ -6,6 +6,8 @@ module Gitlab class CreateGroup < ::BaseService include Stepable + NAME = 'GitLab Instance' + PATH_PREFIX = 'gitlab-instance' VISIBILITY_LEVEL = Gitlab::VisibilityLevel::INTERNAL steps :validate_application_settings, @@ -117,12 +119,12 @@ module Gitlab def create_group_params { - name: 'GitLab Instance Administrators', + name: NAME, visibility_level: VISIBILITY_LEVEL, # The 8 random characters at the end are so that the path does not # clash with any existing group that the user might have created. - path: "gitlab-instance-administrators-#{SecureRandom.hex(4)}" + path: "#{PATH_PREFIX}-#{SecureRandom.hex(4)}" } end end 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 07a4c3bf5e6..88f035c2d1b 100644 --- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb +++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb @@ -9,7 +9,7 @@ module Gitlab include SelfMonitoring::Helpers VISIBILITY_LEVEL = Gitlab::VisibilityLevel::INTERNAL - PROJECT_NAME = 'GitLab self monitoring' + PROJECT_NAME = 'Monitoring' steps :validate_application_settings, :create_group, diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 0d027809ba8..a5259079345 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -60,7 +60,7 @@ module Gitlab # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. if rich_line - line_prefix = diff_line.text =~ /\A(.)/ ? $1 : ' ' + line_prefix = diff_line.text =~ /\A(.)/ ? Regexp.last_match(1) : ' ' "#{line_prefix}#{rich_line}".html_safe end end diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb index 0c3b6b72313..0eb22e6b3cb 100644 --- a/lib/gitlab/diff/highlight_cache.rb +++ b/lib/gitlab/diff/highlight_cache.rb @@ -3,6 +3,7 @@ module Gitlab module Diff class HighlightCache + include Gitlab::Utils::Gzip include Gitlab::Utils::StrongMemoize EXPIRATION = 1.week @@ -83,7 +84,7 @@ module Gitlab redis.hset( key, diff_file_id, - compose_data(highlighted_diff_lines_hash.to_json) + gzip_compress(highlighted_diff_lines_hash.to_json) ) end @@ -145,35 +146,12 @@ module Gitlab end results.map! do |result| - Gitlab::Json.parse(extract_data(result), symbolize_names: true) unless result.nil? + Gitlab::Json.parse(gzip_decompress(result), symbolize_names: true) unless result.nil? end file_paths.zip(results).to_h end - def compose_data(json_data) - # #compress returns ASCII-8BIT, so we need to force the encoding to - # UTF-8 before caching it in redis, else we risk encoding mismatch - # errors. - # - ActiveSupport::Gzip.compress(json_data).force_encoding("UTF-8") - rescue Zlib::GzipFile::Error - json_data - end - - def extract_data(data) - # Since we could be dealing with an already populated cache full of data - # that isn't gzipped, we want to also check to see if the data is - # gzipped before we attempt to #decompress it, thus we check the first - # 2 bytes for "\x1F\x8B" to confirm it is a gzipped string. While a - # non-gzipped string will raise a Zlib::GzipFile::Error, which we're - # rescuing, we don't want to count on rescue for control flow. - # - data[0..1] == "\x1F\x8B" ? ActiveSupport::Gzip.decompress(data) : data - rescue Zlib::GzipFile::Error - data - end - def cacheable?(diff_file) diffable.present? && diff_file.text? && diff_file.diffable? end diff --git a/lib/gitlab/discussions_diff/highlight_cache.rb b/lib/gitlab/discussions_diff/highlight_cache.rb index 4bec6467c1a..3337aeb9262 100644 --- a/lib/gitlab/discussions_diff/highlight_cache.rb +++ b/lib/gitlab/discussions_diff/highlight_cache.rb @@ -3,6 +3,8 @@ module Gitlab module DiscussionsDiff class HighlightCache + extend Gitlab::Utils::Gzip + class << self VERSION = 1 EXPIRATION = 1.week @@ -17,7 +19,7 @@ module Gitlab mapping.each do |raw_key, value| key = cache_key_for(raw_key) - multi.set(key, value.to_json, ex: EXPIRATION) + multi.set(key, gzip_compress(value.to_json), ex: EXPIRATION) end end end @@ -44,7 +46,7 @@ module Gitlab content.map! do |lines| next unless lines - Gitlab::Json.parse(lines).map! do |line| + Gitlab::Json.parse(gzip_decompress(lines)).map! do |line| Gitlab::Diff::Line.safe_init_from_hash(line) end end diff --git a/lib/gitlab/email/hook/disable_email_interceptor.rb b/lib/gitlab/email/hook/disable_email_interceptor.rb index 58dc1527c7a..6e2e0201684 100644 --- a/lib/gitlab/email/hook/disable_email_interceptor.rb +++ b/lib/gitlab/email/hook/disable_email_interceptor.rb @@ -7,7 +7,7 @@ module Gitlab def self.delivering_email(message) message.perform_deliveries = false - Rails.logger.info "Emails disabled! Interceptor prevented sending mail #{message.subject}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.info "Emails disabled! Interceptor prevented sending mail #{message.subject}" end end end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index bf6c28b9f90..f5e47b43a9a 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -54,7 +54,8 @@ module Gitlab def key_from_additional_headers(mail) find_key_from_references(mail) || find_key_from_delivered_to_header(mail) || - find_key_from_envelope_to_header(mail) + find_key_from_envelope_to_header(mail) || + find_key_from_x_envelope_to_header(mail) end def ensure_references_array(references) @@ -91,6 +92,13 @@ module Gitlab end end + def find_key_from_x_envelope_to_header(mail) + Array(mail[:x_envelope_to]).find do |header| + key = Gitlab::IncomingEmail.key_from_address(header.value) + break key if key + end + end + def ignore_auto_reply!(mail) if auto_submitted?(mail) || auto_replied?(mail) raise AutoGeneratedEmailError diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb index 67f8d691a77..7b79de00c66 100644 --- a/lib/gitlab/encoding_helper.rb +++ b/lib/gitlab/encoding_helper.rb @@ -59,7 +59,7 @@ module Gitlab begin CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8') rescue ArgumentError => e - Rails.logger.warn("Ignoring error converting #{detect[:encoding]} into UTF8: #{e.message}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("Ignoring error converting #{detect[:encoding]} into UTF8: #{e.message}") '' end diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb index 8d5611411c9..803acef9a40 100644 --- a/lib/gitlab/error_tracking.rb +++ b/lib/gitlab/error_tracking.rb @@ -26,6 +26,8 @@ module Gitlab # Sanitize fields based on those sanitized from Rails. config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) config.processors << ::Gitlab::ErrorTracking::Processor::SidekiqProcessor + config.processors << ::Gitlab::ErrorTracking::Processor::GrpcErrorProcessor + # Sanitize authentication headers config.sanitize_http_headers = %w[Authorization Private-Token] config.tags = extra_tags_from_env.merge(program: Gitlab.process_name) diff --git a/lib/gitlab/error_tracking/processor/grpc_error_processor.rb b/lib/gitlab/error_tracking/processor/grpc_error_processor.rb new file mode 100644 index 00000000000..871e9c4b7c8 --- /dev/null +++ b/lib/gitlab/error_tracking/processor/grpc_error_processor.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Gitlab + module ErrorTracking + module Processor + class GrpcErrorProcessor < ::Raven::Processor + DEBUG_ERROR_STRING_REGEX = RE2('(.*) debug_error_string:(.*)') + + def process(value) + process_first_exception_value(value) + process_custom_fingerprint(value) + + value + end + + # Sentry can report multiple exceptions in an event. Sanitize + # only the first one since that's what is used for grouping. + def process_first_exception_value(value) + exceptions = value.dig(:exception, :values) + + return unless exceptions.is_a?(Array) + + entry = exceptions.first + + return unless entry.is_a?(Hash) + + exception_type = entry[:type] + raw_message = entry[:value] + + return unless exception_type&.start_with?('GRPC::') + return unless raw_message.present? + + message, debug_str = split_debug_error_string(raw_message) + + entry[:value] = message if message + extra = value[:extra] || {} + extra[:grpc_debug_error_string] = debug_str if debug_str + end + + def process_custom_fingerprint(value) + fingerprint = value[:fingerprint] + + return value unless custom_grpc_fingerprint?(fingerprint) + + message, _ = split_debug_error_string(fingerprint[1]) + fingerprint[1] = message if message + end + + private + + def custom_grpc_fingerprint?(fingerprint) + fingerprint.is_a?(Array) && fingerprint.length == 2 && fingerprint[0].start_with?('GRPC::') + end + + def split_debug_error_string(message) + return unless message + + match = DEBUG_ERROR_STRING_REGEX.match(message) + + return unless match + + [match[1], match[2]] + end + end + end + end +end diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 9908369426a..dca60c93fb2 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -42,9 +42,6 @@ module Gitlab ci_notification_dot: { tracking_category: 'Growth::Expansion::Experiment::CiNotificationDot' }, - buy_ci_minutes_version_a: { - tracking_category: 'Growth::Expansion::Experiment::BuyCiMinutesVersionA' - }, upgrade_link_in_user_menu_a: { tracking_category: 'Growth::Expansion::Experiment::UpgradeLinkInUserMenuA' }, @@ -62,6 +59,9 @@ module Gitlab }, customize_homepage: { tracking_category: 'Growth::Expansion::Experiment::CustomizeHomepage' + }, + invite_email: { + tracking_category: 'Growth::Acquisition::Experiment::InviteEmail' } }.freeze @@ -78,7 +78,7 @@ module Gitlab included do before_action :set_experimentation_subject_id_cookie, unless: :dnt_enabled? - helper_method :experiment_enabled? + helper_method :experiment_enabled?, :experiment_tracking_category_and_group end def set_experimentation_subject_id_cookie @@ -118,6 +118,10 @@ module Gitlab ::Experiment.add_user(experiment_key, tracking_group(experiment_key), current_user) end + def experiment_tracking_category_and_group(experiment_key) + "#{tracking_category(experiment_key)}:#{tracking_group(experiment_key, '_group')}" + end + private def dnt_enabled? @@ -144,7 +148,7 @@ module Gitlab { category: tracking_category(experiment_key), action: action, - property: "#{tracking_group(experiment_key)}_group", + property: tracking_group(experiment_key, "_group"), label: experimentation_subject_id, value: value }.compact @@ -154,10 +158,12 @@ module Gitlab Experimentation.experiment(experiment_key).tracking_category end - def tracking_group(experiment_key) + def tracking_group(experiment_key, suffix = nil) return unless Experimentation.enabled?(experiment_key) - experiment_enabled?(experiment_key) ? GROUP_EXPERIMENTAL : GROUP_CONTROL + group = experiment_enabled?(experiment_key) ? GROUP_EXPERIMENTAL : GROUP_CONTROL + + suffix ? "#{group}#{suffix}" : group end def forced_enabled?(experiment_key) diff --git a/lib/gitlab/file_type_detection.rb b/lib/gitlab/file_type_detection.rb index 475d50e37bf..38ccd2c38a9 100644 --- a/lib/gitlab/file_type_detection.rb +++ b/lib/gitlab/file_type_detection.rb @@ -20,6 +20,8 @@ module Gitlab module FileTypeDetection SAFE_IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze + SAFE_IMAGE_FOR_SCALING_EXT = %w[png jpg jpeg].freeze + PDF_EXT = 'pdf' # We recommend using the .mp4 format over .mov. Videos in .mov format can # still be used but you really need to make sure they are served with the @@ -46,6 +48,12 @@ module Gitlab extension_match?(SAFE_IMAGE_EXT) end + # For the time being, we restrict image scaling requests to the most popular and safest formats only, + # which are JPGs and PNGs. See https://gitlab.com/gitlab-org/gitlab/-/issues/237848 for more info. + def image_safe_for_scaling? + extension_match?(SAFE_IMAGE_FOR_SCALING_EXT) + end + def video? extension_match?(SAFE_VIDEO_EXT) end diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 2c53f9b026d..bd5d2e53180 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -39,7 +39,7 @@ module Gitlab def user_map @user_map ||= begin - user_map = Hash.new + user_map = {} 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 diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index b6bffb11344..96f3487fd6f 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -13,7 +13,6 @@ module Gitlab TAG_REF_PREFIX = "refs/tags/" BRANCH_REF_PREFIX = "refs/heads/" - BaseError = Class.new(StandardError) CommandError = Class.new(BaseError) CommitError = Class.new(BaseError) OSError = Class.new(BaseError) diff --git a/lib/gitlab/git/base_error.rb b/lib/gitlab/git/base_error.rb new file mode 100644 index 00000000000..a7eaa82b347 --- /dev/null +++ b/lib/gitlab/git/base_error.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Git + class BaseError < StandardError + DEBUG_ERROR_STRING_REGEX = /(.*?) debug_error_string:.*$/m.freeze + + def initialize(msg = nil) + if msg + raw_message = msg.to_s + match = DEBUG_ERROR_STRING_REGEX.match(raw_message) + raw_message = match[1] if match + + super(raw_message) + else + super + end + end + end + end +end diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index 46896961867..e6121d688ba 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -37,7 +37,7 @@ module Gitlab @byte_count = 0 @overflow = false @empty = true - @array = Array.new + @array = [] end def each(&block) diff --git a/lib/gitlab/git/keep_around.rb b/lib/gitlab/git/keep_around.rb index d58f10bdbb7..b6fc335c979 100644 --- a/lib/gitlab/git/keep_around.rb +++ b/lib/gitlab/git/keep_around.rb @@ -27,7 +27,7 @@ module Gitlab # This will still fail if the file is corrupted (e.g. 0 bytes) raw_repository.write_ref(keep_around_ref_name(sha), sha) rescue Gitlab::Git::CommandError => ex - Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}" end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 596b4e9f692..8ace4157ad7 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -19,15 +19,15 @@ module Gitlab GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000' - NoRepository = Class.new(StandardError) - InvalidRepository = Class.new(StandardError) - InvalidBlobName = Class.new(StandardError) - InvalidRef = Class.new(StandardError) - GitError = Class.new(StandardError) - DeleteBranchError = Class.new(StandardError) - TagExistsError = Class.new(StandardError) - ChecksumError = Class.new(StandardError) - class CreateTreeError < StandardError + NoRepository = Class.new(::Gitlab::Git::BaseError) + InvalidRepository = Class.new(::Gitlab::Git::BaseError) + InvalidBlobName = Class.new(::Gitlab::Git::BaseError) + InvalidRef = Class.new(::Gitlab::Git::BaseError) + GitError = Class.new(::Gitlab::Git::BaseError) + DeleteBranchError = Class.new(::Gitlab::Git::BaseError) + TagExistsError = Class.new(::Gitlab::Git::BaseError) + ChecksumError = Class.new(::Gitlab::Git::BaseError) + class CreateTreeError < ::Gitlab::Git::BaseError attr_reader :error_code def initialize(error_code) @@ -955,7 +955,7 @@ module Gitlab gitaly_repository_client.cleanup if exists? end rescue Gitlab::Git::CommandError => e # Don't fail if we can't cleanup - Rails.logger.error("Unable to clean repository on storage #{storage} with relative path #{relative_path}: #{e.message}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error("Unable to clean repository on storage #{storage} with relative path #{relative_path}: #{e.message}") Gitlab::Metrics.counter( :failed_repository_cleanup_total, 'Number of failed repository cleanup events' diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index 76771f0417b..da2d015ca4a 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -6,7 +6,6 @@ module Gitlab include Gitlab::Git::WrapsGitalyErrors DuplicatePageError = Class.new(StandardError) - OperationError = Class.new(StandardError) DEFAULT_PAGINATION = Kaminari.config.default_per_page diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb index f2b4e930707..ae83e45f2b3 100644 --- a/lib/gitlab/git_access_snippet.rb +++ b/lib/gitlab/git_access_snippet.rb @@ -119,7 +119,7 @@ module Gitlab override :check_single_change_access def check_single_change_access(change, _skip_lfs_integrity_check: false) - Checks::SnippetCheck.new(change, logger: logger).validate! + Checks::SnippetCheck.new(change, default_branch: snippet.default_branch, logger: logger).validate! Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit(user), logger: logger).validate! rescue Checks::TimedLogger::TimeoutError raise TimeoutError, logger.full_message diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 131c00db612..e1324530412 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -450,7 +450,7 @@ module Gitlab stack_string = Gitlab::BacktraceCleaner.clean_backtrace(caller).drop(1).join("\n") - Gitlab::SafeRequestStore[:stack_counter] ||= Hash.new + Gitlab::SafeRequestStore[:stack_counter] ||= {} count = Gitlab::SafeRequestStore[:stack_counter][stack_string] || 0 Gitlab::SafeRequestStore[:stack_counter][stack_string] = count + 1 diff --git a/lib/gitlab/gitpod.rb b/lib/gitlab/gitpod.rb new file mode 100644 index 00000000000..11b54db72ea --- /dev/null +++ b/lib/gitlab/gitpod.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + class Gitpod + class << self + def feature_conditional? + feature.conditional? + end + + def feature_available? + # The gitpod_bundle feature could be conditionally applied, so check if `!off?` + !feature.off? + end + + def feature_enabled?(actor = nil) + feature.enabled?(actor) + end + + def feature_and_settings_enabled?(actor = nil) + feature_enabled?(actor) && Gitlab::CurrentSettings.gitpod_enabled + end + + private + + def feature + Feature.get(:gitpod) # rubocop:disable Gitlab/AvoidFeatureGet + end + end + end +end diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb index 7346de13626..54dca93a891 100644 --- a/lib/gitlab/gl_repository.rb +++ b/lib/gitlab/gl_repository.rb @@ -7,19 +7,19 @@ module Gitlab PROJECT = RepoType.new( name: :project, access_checker_class: Gitlab::GitAccessProject, - repository_resolver: -> (project) { project&.repository } + repository_resolver: -> (project) { ::Repository.new(project.full_path, project, shard: project.repository_storage, disk_path: project.disk_path) } ).freeze WIKI = RepoType.new( name: :wiki, access_checker_class: Gitlab::GitAccessWiki, - repository_resolver: -> (container) { container&.wiki&.repository }, + repository_resolver: -> (container) { ::Repository.new(container.wiki.full_path, container, shard: container.wiki.repository_storage, disk_path: container.wiki.disk_path, repo_type: WIKI) }, project_resolver: -> (container) { container.is_a?(Project) ? container : nil }, suffix: :wiki ).freeze SNIPPET = RepoType.new( name: :snippet, access_checker_class: Gitlab::GitAccessSnippet, - repository_resolver: -> (snippet) { snippet&.repository }, + repository_resolver: -> (snippet) { ::Repository.new(snippet.full_path, snippet, shard: snippet.repository_storage, disk_path: snippet.disk_path, repo_type: SNIPPET) }, container_class: Snippet, project_resolver: -> (snippet) { snippet&.project }, guest_read_ability: :read_snippet diff --git a/lib/gitlab/gl_repository/repo_type.rb b/lib/gitlab/gl_repository/repo_type.rb index 2c0038b61e2..346f6be0d98 100644 --- a/lib/gitlab/gl_repository/repo_type.rb +++ b/lib/gitlab/gl_repository/repo_type.rb @@ -57,6 +57,8 @@ module Gitlab end def repository_for(container) + return unless container + repository_resolver.call(container) end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index dfba68ce899..66517ecd743 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -4,6 +4,7 @@ module Gitlab module GonHelper + include StartupCssHelper include WebpackHelper def add_gon_variables @@ -45,9 +46,12 @@ module Gitlab push_frontend_feature_flag(:snippets_vue, default_enabled: true) push_frontend_feature_flag(:monaco_blobs, default_enabled: true) push_frontend_feature_flag(:monaco_ci, default_enabled: false) - push_frontend_feature_flag(:snippets_edit_vue, default_enabled: false) + push_frontend_feature_flag(:snippets_edit_vue, default_enabled: true) push_frontend_feature_flag(:webperf_experiment, default_enabled: false) push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false) + + # Startup CSS feature is a special one as it can be enabled by means of cookies and params + gon.push({ features: { 'startupCss' => use_startup_css? } }, true) end # Exposes the state of a feature flag to the frontend code. diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb index 0dd28b32511..dcd0e12cbfc 100644 --- a/lib/gitlab/graphql/docs/helper.rb +++ b/lib/gitlab/graphql/docs/helper.rb @@ -21,30 +21,47 @@ module Gitlab MD end - def sorted_fields(fields) - fields.sort_by { |field| field[:name] } + def render_name_and_description(object) + content = "### #{object[:name]}\n" + + if object[:description].present? + content += "\n#{object[:description]}.\n" + end + + content + end + + def sorted_by_name(objects) + objects.sort_by { |o| o[:name] } end def render_field(field) '| %s | %s | %s |' % [ - render_field_name(field), + render_name(field), render_field_type(field[:type][:info]), - render_field_description(field) + render_description(field) ] end - def render_field_name(field) - rendered_name = "`#{field[:name]}`" - rendered_name += ' **{warning-solid}**' if field[:is_deprecated] + def render_enum_value(value) + '| %s | %s |' % [ + render_name(value), + render_description(value) + ] + end + + def render_name(object) + rendered_name = "`#{object[:name]}`" + rendered_name += ' **{warning-solid}**' if object[:is_deprecated] rendered_name end - # Returns the field description. If the field has been deprecated, + # Returns the object description. If the object has been deprecated, # the deprecation reason will be returned in place of the description. - def render_field_description(field) - return field[:description] unless field[:is_deprecated] + def render_description(object) + return object[:description] unless object[:is_deprecated] - "**Deprecated:** #{field[:deprecation_reason]}" + "**Deprecated:** #{object[:deprecation_reason]}" end # Some fields types are arrays of other types and are displayed @@ -70,6 +87,13 @@ module Gitlab !object_type[:name]["__"] end end + + # We ignore the built-in enum types. + def enums + graphql_enum_types.select do |enum_type| + !enum_type[:name].in?(%w(__DirectiveLocation __TypeKind)) + end + end end end end diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml index 8c033526557..ec052943589 100644 --- a/lib/gitlab/graphql/docs/templates/default.md.haml +++ b/lib/gitlab/graphql/docs/templates/default.md.haml @@ -15,15 +15,45 @@ CAUTION: **Caution:** Fields that are deprecated are marked with **{warning-solid}**. \ + +:plain + ## Object types + + Object types represent the resources that GitLab's 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. + + For more information, see + [Object Types and Fields](https://graphql.org/learn/schema/#object-types-and-fields) + on `graphql.org`. +\ + - objects.each do |type| - unless type[:fields].empty? - = "## #{type[:name]}" - - if type[:description]&.present? - \ - = type[:description] - \ - ~ "| Name | Type | Description |" - ~ "| --- | ---- | ---------- |" - - sorted_fields(type[:fields]).each do |field| + = render_name_and_description(type) + ~ "| Field | Type | Description |" + ~ "| ----- | ---- | ----------- |" + - sorted_by_name(type[:fields]).each do |field| = render_field(field) \ + +:plain + ## Enumeration types + + Also called _Enums_, enumeration types are a special kind of scalar that + is restricted to a particular set of allowed values. + + For more information, see + [Enumeration Types](https://graphql.org/learn/schema/#enumeration-types) + on `graphql.org`. +\ + +- enums.each do |enum| + - unless enum[:values].empty? + = render_name_and_description(enum) + ~ "| Value | Description |" + ~ "| ----- | ----------- |" + - sorted_by_name(enum[:values]).each do |value| + = render_enum_value(value) + \ diff --git a/lib/gitlab/graphql/loaders/issuable_loader.rb b/lib/gitlab/graphql/loaders/issuable_loader.rb index 1cc0fbe215f..8ac4be2b661 100644 --- a/lib/gitlab/graphql/loaders/issuable_loader.rb +++ b/lib/gitlab/graphql/loaders/issuable_loader.rb @@ -15,6 +15,7 @@ module Gitlab def batching_find_all(&with_query) if issuable_finder.params.keys == ['iids'] + issuable_finder.parent = parent batch_load_issuables(issuable_finder.params[:iids], with_query) else post_process(find_all, with_query) @@ -22,24 +23,12 @@ module Gitlab end def find_all - issuable_finder.params[parent_param] = parent if parent - + issuable_finder.parent_param = parent if parent issuable_finder.execute end private - def parent_param - case parent - when Project - :project_id - when Group - :group_id - else - raise "Unexpected parent: #{parent.class}" - end - end - def post_process(query, with_query) if with_query with_query.call(query) @@ -56,7 +45,7 @@ module Gitlab return if parent.nil? BatchLoader::GraphQL - .for([parent_param, iid.to_s]) + .for([issuable_finder.parent_param, iid.to_s]) .batch(key: batch_key) do |params, loader, args| batch_key = args[:key] user = batch_key.current_user diff --git a/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb b/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb index afea7c602be..bd785880b57 100644 --- a/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb +++ b/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb @@ -29,7 +29,10 @@ module Gitlab def table_condition(order_info, value, operator) if order_info.named_function target = order_info.named_function - value = value&.downcase if target&.name&.downcase == 'lower' + + if target.try(:name)&.casecmp('lower') == 0 + value = value&.downcase + end else target = arel_table[order_info.attribute_name] end diff --git a/lib/gitlab/graphql/pagination/keyset/order_info.rb b/lib/gitlab/graphql/pagination/keyset/order_info.rb index 12bcc4993b5..f54695ddb9a 100644 --- a/lib/gitlab/graphql/pagination/keyset/order_info.rb +++ b/lib/gitlab/graphql/pagination/keyset/order_info.rb @@ -71,25 +71,43 @@ module Gitlab def extract_nulls_last_order(order_value) tokens = order_value.downcase.split - [tokens.first, (tokens[1] == 'asc' ? :asc : :desc), nil] + column_reference = tokens.first + sort_direction = tokens[1] == 'asc' ? :asc : :desc + + # Handles the case when the order value is coming from another table. + # Example: table_name.column_name + # Query the value using the fully qualified column name: pass table_name.column_name as the named_function + if fully_qualified_column_reference?(column_reference) + [column_reference, sort_direction, Arel.sql(column_reference)] + else + [column_reference, sort_direction, nil] + end + end + + # Example: table_name.column_name + def fully_qualified_column_reference?(attribute) + attribute.to_s.count('.') == 1 end def extract_attribute_values(order_value) - named = nil - name = if ordering_by_lower?(order_value) - named = order_value.expr - named.expressions[0].name.to_s - else - order_value.expr.name - end - - [name, order_value.direction, named] + if ordering_by_lower?(order_value) + [order_value.expr.expressions[0].name.to_s, order_value.direction, order_value.expr] + elsif ordering_by_similarity?(order_value) + ['similarity', order_value.direction, order_value.expr] + else + [order_value.expr.name, order_value.direction, nil] + end end # determine if ordering using LOWER, eg. "ORDER BY LOWER(boards.name)" def ordering_by_lower?(order_value) order_value.expr.is_a?(Arel::Nodes::NamedFunction) && order_value.expr&.name&.downcase == 'lower' end + + # determine if ordering using SIMILARITY scoring based on Gitlab::Database::SimilarityScore + def ordering_by_similarity?(order_value) + order_value.to_sql.match?(/SIMILARITY\(.+\*/) + end end end end diff --git a/lib/gitlab/graphql/representation/submodule_tree_entry.rb b/lib/gitlab/graphql/representation/submodule_tree_entry.rb index 8d17cb9eecc..aa5e74cc837 100644 --- a/lib/gitlab/graphql/representation/submodule_tree_entry.rb +++ b/lib/gitlab/graphql/representation/submodule_tree_entry.rb @@ -24,11 +24,11 @@ module Gitlab end def web_url - @submodule_links.first + @submodule_links&.web end def tree_url - @submodule_links.last + @submodule_links&.tree end end end diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb index eb4361cdc53..0cc3de297ba 100644 --- a/lib/gitlab/group_search_results.rb +++ b/lib/gitlab/group_search_results.rb @@ -4,28 +4,32 @@ module Gitlab class GroupSearchResults < SearchResults attr_reader :group - def initialize(current_user, limit_projects, group, query, default_project_filter: false) - super(current_user, limit_projects, query, default_project_filter: default_project_filter) - + def initialize(current_user, query, limit_projects = nil, group:, default_project_filter: false, filters: {}) @group = group + + super(current_user, query, limit_projects, default_project_filter: default_project_filter, filters: filters) end # rubocop:disable CodeReuse/ActiveRecord def users - # 1: get all groups the current user has access to - groups = GroupsFinder.new(current_user).execute.joins(:users) + # get all groups the current user has access to + # ignore order inherited from GroupsFinder to improve performance + current_user_groups = GroupsFinder.new(current_user).execute.unscope(:order) + + # the hierarchy of the current group + group_groups = @group.self_and_hierarchy.unscope(:order) + + # the groups where the above hierarchies intersect + intersect_groups = group_groups.where(id: current_user_groups) - # 2: Get the group's whole hierarchy - group_users = @group.direct_and_indirect_users + # members of @group hierarchy where the user has access to the groups + members = GroupMember.where(group: intersect_groups).non_invite - # 3: get all users the current user has access to (-> - # `SearchResults#users`), which also applies the query. + # get all users the current user has access to (-> `SearchResults#users`), which also applies the query users = super - # 4: filter for users that belong to the previously selected groups - users - .where(id: group_users.select('id')) - .where(id: groups.select('members.user_id')) + # filter users that belong to the previously selected groups + users.where(id: members.select(:user_id)) end # rubocop:enable CodeReuse/ActiveRecord diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb index b72d08549fe..b57560544c8 100644 --- a/lib/gitlab/hashed_storage/migrator.rb +++ b/lib/gitlab/hashed_storage/migrator.rb @@ -62,28 +62,24 @@ module Gitlab # Flag a project to be migrated to Hashed Storage # # @param [Project] project that will be migrated - # rubocop:disable Gitlab/RailsLogger def migrate(project) - Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..." + Gitlab::AppLogger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..." project.migrate_to_hashed_storage! rescue => err - Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}") + Gitlab::AppLogger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}") end - # rubocop:enable Gitlab/RailsLogger # Flag a project to be rolled-back to Legacy Storage # # @param [Project] project that will be rolled-back - # rubocop:disable Gitlab/RailsLogger def rollback(project) - Rails.logger.info "Starting storage rollback of #{project.full_path} (ID=#{project.id})..." + Gitlab::AppLogger.info "Starting storage rollback of #{project.full_path} (ID=#{project.id})..." project.rollback_to_legacy_storage! rescue => err - Rails.logger.error("#{err.message} rolling-back storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}") + Gitlab::AppLogger.error("#{err.message} rolling-back storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}") end - # rubocop:enable Gitlab/RailsLogger # Returns whether we have any pending storage migration # @@ -97,6 +93,14 @@ module Gitlab any_non_empty_queue?(::HashedStorage::RollbackerWorker, ::HashedStorage::ProjectRollbackWorker) end + # Remove all remaining scheduled rollback operations + # + def abort_rollback! + [::HashedStorage::RollbackerWorker, ::HashedStorage::ProjectRollbackWorker].each do |worker| + Sidekiq::Queue.new(worker.queue).clear + end + end + private def any_non_empty_queue?(*workers) diff --git a/lib/gitlab/health_checks/simple_abstract_check.rb b/lib/gitlab/health_checks/simple_abstract_check.rb index 4e0b9296819..ae99768b7b4 100644 --- a/lib/gitlab/health_checks/simple_abstract_check.rb +++ b/lib/gitlab/health_checks/simple_abstract_check.rb @@ -24,7 +24,7 @@ module Gitlab result, elapsed = with_timing(&method(:check)) return if result.nil? - Rails.logger.error("#{human_name} check returned unexpected result #{result}") unless successful?(result) # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error("#{human_name} check returned unexpected result #{result}") unless successful?(result) [ metric("#{metric_prefix}_timeout", result.is_a?(Timeout::Error) ? 1 : 0), metric("#{metric_prefix}_success", successful?(result) ? 1 : 0), diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 07c43fa4832..40dee0142b9 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -3,8 +3,8 @@ module Gitlab class Highlight TIMEOUT_BACKGROUND = 30.seconds - TIMEOUT_FOREGROUND = 3.seconds - MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte + TIMEOUT_FOREGROUND = 1.5.seconds + MAXIMUM_TEXT_HIGHLIGHT_SIZE = 512.kilobytes def self.highlight(blob_name, blob_content, language: nil, plain: false) new(blob_name, blob_content, language: language) @@ -31,8 +31,8 @@ module Gitlab def lexer @lexer ||= custom_language || begin Rouge::Lexer.guess(filename: @blob_name, source: @blob_content).new - rescue Rouge::Guesser::Ambiguous => e - e.alternatives.min_by(&:tag) + rescue Rouge::Guesser::Ambiguous => e + e.alternatives.min_by(&:tag) end end diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb index 559e1828a70..be87dcc0ff9 100644 --- a/lib/gitlab/http.rb +++ b/lib/gitlab/http.rb @@ -35,7 +35,7 @@ module Gitlab def self.perform_request(http_method, path, options, &block) log_info = options.delete(:extra_log_info) options_with_timeouts = - if !options.has_key?(:timeout) && Feature.enabled?(:http_default_timeouts) + if !options.has_key?(:timeout) options.with_defaults(DEFAULT_TIMEOUT_OPTIONS) else options diff --git a/lib/gitlab/i18n/html_todo.yml b/lib/gitlab/i18n/html_todo.yml index bfd96ba8579..91e01f8a0b8 100644 --- a/lib/gitlab/i18n/html_todo.yml +++ b/lib/gitlab/i18n/html_todo.yml @@ -16,67 +16,6 @@ # why this change has been made. # -" 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시간" - # # 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: @@ -313,3 +252,63 @@ - "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/import_export/lfs_saver.rb b/lib/gitlab/import_export/lfs_saver.rb index 18c590e1ca9..515fd98630c 100644 --- a/lib/gitlab/import_export/lfs_saver.rb +++ b/lib/gitlab/import_export/lfs_saver.rb @@ -16,7 +16,7 @@ module Gitlab end def save - project.all_lfs_objects.find_in_batches(batch_size: BATCH_SIZE) do |batch| + project.lfs_objects.find_in_batches(batch_size: BATCH_SIZE) do |batch| batch.each do |lfs_object| save_lfs_object(lfs_object) end diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index a240c367a42..9ec5df8cde9 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -190,8 +190,20 @@ excluded_attributes: - :stored_externally - :external_diff_store - :merge_request_id + - :verification_retry_at + - :verified_at + - :verification_retry_count + - :verification_checksum + - :verification_failure merge_request_diff_commits: - :merge_request_diff_id + merge_request_diff_detail: + - :merge_request_diff_id + - :verification_retry_at + - :verified_at + - :verification_retry_count + - :verification_checksum + - :verification_failure merge_request_diff_files: - :diff - :external_diff_offset diff --git a/lib/gitlab/jira/dvcs.rb b/lib/gitlab/jira/dvcs.rb new file mode 100644 index 00000000000..4415f98fc7f --- /dev/null +++ b/lib/gitlab/jira/dvcs.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Gitlab + module Jira + module Dvcs + ENCODED_SLASH = '@'.freeze + SLASH = '/'.freeze + ENCODED_ROUTE_REGEX = /[a-zA-Z0-9_\-\.#{ENCODED_SLASH}]+/.freeze + + def self.encode_slash(path) + path.gsub(SLASH, ENCODED_SLASH) + end + + def self.decode_slash(path) + path.gsub(ENCODED_SLASH, SLASH) + end + + # To present two types of projects stored by Jira, + # Type 1 are projects imported prior to nested group support, + # those project names are not full_path, so they are presented differently + # to maintain backwards compatibility. + # Type 2 are projects imported after nested group support, + # those project names are encoded full path + # + # @param [Project] project + def self.encode_project_name(project) + if project.namespace.has_parent? + encode_slash(project.full_path) + else + project.path + end + end + + # To interpret two types of project names stored by Jira (see `encode_project_name`) + # + # @param [String] project + # Either an encoded full path, or just project name + # @param [String] namespace + def self.restore_full_path(namespace:, project:) + if project.include?(ENCODED_SLASH) + project.gsub(ENCODED_SLASH, SLASH) + else + "#{namespace}/#{project}" + end + end + end + end +end diff --git a/lib/gitlab/jira/middleware.rb b/lib/gitlab/jira/middleware.rb new file mode 100644 index 00000000000..8a74729da49 --- /dev/null +++ b/lib/gitlab/jira/middleware.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Jira + class Middleware + def self.jira_dvcs_connector?(env) + env['HTTP_USER_AGENT']&.downcase&.start_with?('jira dvcs connector') + end + + def initialize(app) + @app = app + end + + def call(env) + if self.class.jira_dvcs_connector?(env) + env['HTTP_AUTHORIZATION'] = env['HTTP_AUTHORIZATION']&.sub('token', 'Bearer') + end + + @app.call(env) + end + end + end +end diff --git a/lib/gitlab/json.rb b/lib/gitlab/json.rb index d6681347f42..29cfec443e8 100644 --- a/lib/gitlab/json.rb +++ b/lib/gitlab/json.rb @@ -22,7 +22,7 @@ module Gitlab string = string.to_s unless string.is_a?(String) legacy_mode = legacy_mode_enabled?(opts.delete(:legacy_mode)) - data = adapter_load(string, opts) + data = adapter_load(string, **opts) handle_legacy_mode!(data) if legacy_mode diff --git a/lib/gitlab/kas.rb b/lib/gitlab/kas.rb new file mode 100644 index 00000000000..08dde98e965 --- /dev/null +++ b/lib/gitlab/kas.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module Kas + INTERNAL_API_REQUEST_HEADER = 'Gitlab-Kas-Api-Request' + JWT_ISSUER = 'gitlab-kas' + + include JwtAuthenticatable + + class << self + def verify_api_request(request_headers) + decode_jwt_for_issuer(JWT_ISSUER, request_headers[INTERNAL_API_REQUEST_HEADER]) + rescue JWT::DecodeError + nil + end + + def secret_path + Gitlab.config.gitlab_kas.secret_file + end + + def ensure_secret! + return if File.exist?(secret_path) + + write_secret + end + end + end +end diff --git a/lib/gitlab/kubernetes/cilium_network_policy.rb b/lib/gitlab/kubernetes/cilium_network_policy.rb index 55afd2b586e..9043932bbe5 100644 --- a/lib/gitlab/kubernetes/cilium_network_policy.rb +++ b/lib/gitlab/kubernetes/cilium_network_policy.rb @@ -9,8 +9,12 @@ module Gitlab API_VERSION = "cilium.io/v2" KIND = 'CiliumNetworkPolicy' - def initialize(name:, namespace:, selector:, ingress:, resource_version:, labels: nil, creation_timestamp: nil, egress: nil) + # We are modeling existing kubernetes resource and don't have + # control over amount of parameters. + # rubocop:disable Metrics/ParameterLists + def initialize(name:, namespace:, selector:, ingress:, resource_version: nil, description: nil, labels: nil, creation_timestamp: nil, egress: nil) @name = name + @description = description @namespace = namespace @labels = labels @creation_timestamp = creation_timestamp @@ -19,15 +23,7 @@ module Gitlab @ingress = ingress @egress = egress end - - def generate - ::Kubeclient::Resource.new.tap do |resource| - resource.kind = KIND - resource.apiVersion = API_VERSION - resource.metadata = metadata - resource.spec = spec - end - end + # rubocop:enable Metrics/ParameterLists def self.from_yaml(manifest) return unless manifest @@ -39,6 +35,7 @@ module Gitlab spec = policy[:spec] self.new( name: metadata[:name], + description: policy[:description], namespace: metadata[:namespace], resource_version: metadata[:resourceVersion], labels: metadata[:labels], @@ -58,6 +55,7 @@ module Gitlab spec = resource[:spec].to_h self.new( name: metadata[:name], + description: resource[:description], namespace: metadata[:namespace], resource_version: metadata[:resourceVersion], labels: metadata[:labels]&.to_h, @@ -68,21 +66,39 @@ module Gitlab ) end + override :resource + def resource + resource = { + apiVersion: API_VERSION, + kind: KIND, + metadata: metadata, + spec: spec + } + resource[:description] = description if description + resource + end + private - attr_reader :name, :namespace, :labels, :creation_timestamp, :resource_version, :ingress, :egress + attr_reader :name, :description, :namespace, :labels, :creation_timestamp, :resource_version, :ingress, :egress def selector @selector ||= {} end - override :spec + def metadata + meta = { name: name, namespace: namespace } + meta[:labels] = labels if labels + meta[:resourceVersion] = resource_version if resource_version + meta + end + def spec { endpointSelector: selector, ingress: ingress, egress: egress - } + }.compact end end end diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb index 9e3cf58bb1e..fa68afd39f5 100644 --- a/lib/gitlab/kubernetes/kube_client.rb +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -92,6 +92,7 @@ module Gitlab # group client delegate :create_network_policy, :get_network_policies, + :get_network_policy, :update_network_policy, :delete_network_policy, to: :networking_client @@ -100,6 +101,7 @@ module Gitlab # group client delegate :create_cilium_network_policy, :get_cilium_network_policies, + :get_cilium_network_policy, :update_cilium_network_policy, :delete_cilium_network_policy, to: :cilium_networking_client diff --git a/lib/gitlab/kubernetes/network_policy.rb b/lib/gitlab/kubernetes/network_policy.rb index 28810dc4453..c8e9b987443 100644 --- a/lib/gitlab/kubernetes/network_policy.rb +++ b/lib/gitlab/kubernetes/network_policy.rb @@ -6,6 +6,8 @@ module Gitlab include NetworkPolicyCommon extend ::Gitlab::Utils::Override + KIND = 'NetworkPolicy' + def initialize(name:, namespace:, selector:, ingress:, labels: nil, creation_timestamp: nil, policy_types: ["Ingress"], egress: nil) @name = name @namespace = namespace @@ -17,13 +19,6 @@ module Gitlab @egress = egress end - def generate - ::Kubeclient::Resource.new.tap do |resource| - resource.metadata = metadata - resource.spec = spec - end - end - def self.from_yaml(manifest) return unless manifest @@ -63,6 +58,15 @@ module Gitlab ) end + override :resource + def resource + { + kind: KIND, + metadata: metadata, + spec: spec + } + end + private attr_reader :name, :namespace, :labels, :creation_timestamp, :policy_types, :ingress, :egress @@ -71,7 +75,12 @@ module Gitlab @selector ||= {} end - override :spec + def metadata + meta = { name: name, namespace: namespace } + meta[:labels] = labels if labels + meta + end + def spec { podSelector: selector, diff --git a/lib/gitlab/kubernetes/network_policy_common.rb b/lib/gitlab/kubernetes/network_policy_common.rb index 3b6e46d21ef..99517454508 100644 --- a/lib/gitlab/kubernetes/network_policy_common.rb +++ b/lib/gitlab/kubernetes/network_policy_common.rb @@ -5,6 +5,10 @@ module Gitlab module NetworkPolicyCommon DISABLED_BY_LABEL = :'network-policy.gitlab.com/disabled_by' + def generate + ::Kubeclient::Resource.new(resource) + end + def as_json(opts = nil) { name: name, @@ -46,19 +50,12 @@ module Gitlab private - def metadata - meta = { name: name, namespace: namespace } - meta[:labels] = labels if labels - meta[:resourceVersion] = resource_version if defined?(resource_version) - meta - end - - def spec + def resource raise NotImplementedError end def manifest - YAML.dump({ metadata: metadata, spec: spec }.deep_stringify_keys) + YAML.dump(resource.deep_stringify_keys) end end end diff --git a/lib/gitlab/lfs/client.rb b/lib/gitlab/lfs/client.rb new file mode 100644 index 00000000000..e4d600694c2 --- /dev/null +++ b/lib/gitlab/lfs/client.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true +module Gitlab + module Lfs + # Gitlab::Lfs::Client implements a simple LFS client, designed to talk to + # LFS servers as described in these documents: + # * https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md + # * https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md + class Client + attr_reader :base_url + + def initialize(base_url, credentials:) + @base_url = base_url + @credentials = credentials + end + + def batch(operation, objects) + body = { + operation: operation, + transfers: ['basic'], + # We don't know `ref`, so can't send it + objects: objects.map { |object| { oid: object.oid, size: object.size } } + } + + rsp = Gitlab::HTTP.post( + batch_url, + basic_auth: basic_auth, + body: body.to_json, + headers: { 'Content-Type' => 'application/vnd.git-lfs+json' } + ) + + raise BatchSubmitError unless rsp.success? + + # HTTParty provides rsp.parsed_response, but it only kicks in for the + # application/json content type in the response, which we can't rely on + body = Gitlab::Json.parse(rsp.body) + transfer = body.fetch('transfer', 'basic') + + raise UnsupportedTransferError.new(transfer.inspect) unless transfer == 'basic' + + body + end + + def upload(object, upload_action, authenticated:) + file = object.file.open + + params = { + body_stream: file, + headers: { + 'Content-Length' => object.size.to_s, + 'Content-Type' => 'application/octet-stream' + }.merge(upload_action['header'] || {}) + } + + params[:basic_auth] = basic_auth unless authenticated + + rsp = Gitlab::HTTP.put(upload_action['href'], params) + + raise ObjectUploadError unless rsp.success? + ensure + file&.close + end + + private + + attr_reader :credentials + + def batch_url + base_url + '/info/lfs/objects/batch' + end + + def basic_auth + return unless credentials[:auth_method] == "password" + + { username: credentials[:user], password: credentials[:password] } + end + + class BatchSubmitError < StandardError + def message + "Failed to submit batch" + end + end + + class UnsupportedTransferError < StandardError + def initialize(transfer = nil) + super + @transfer = transfer + end + + def message + "Unsupported transfer: #{@transfer}" + end + end + + class ObjectUploadError < StandardError + def message + "Failed to upload object" + end + end + end + end +end diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb index 2ec8c268d09..89a4e36a232 100644 --- a/lib/gitlab/logger.rb +++ b/lib/gitlab/logger.rb @@ -32,7 +32,8 @@ module Gitlab end def self.build - Gitlab::SafeRequestStore[self.cache_key] ||= new(self.full_log_path) + Gitlab::SafeRequestStore[self.cache_key] ||= + new(self.full_log_path, level: ::Logger::DEBUG) end def self.full_log_path diff --git a/lib/gitlab/marginalia/comment.rb b/lib/gitlab/marginalia/comment.rb index d5dae5ef4b3..7b4e4b06f00 100644 --- a/lib/gitlab/marginalia/comment.rb +++ b/lib/gitlab/marginalia/comment.rb @@ -28,7 +28,7 @@ module Gitlab # We are using 'Marginalia::SidekiqInstrumentation' which does not support 'ActiveJob::Base'. # Gitlab also uses 'ActionMailer::MailDeliveryJob' which inherits from ActiveJob::Base. # So below condition is used to return metadata for such jobs. - if job.is_a?(ActionMailer::MailDeliveryJob) || job.is_a?(ActionMailer::DeliveryJob) + if job.is_a?(ActionMailer::MailDeliveryJob) { "class" => job.arguments.first, "jid" => job.job_id diff --git a/lib/gitlab/metrics/dashboard/importer.rb b/lib/gitlab/metrics/dashboard/importer.rb new file mode 100644 index 00000000000..ca835650648 --- /dev/null +++ b/lib/gitlab/metrics/dashboard/importer.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Gitlab + module Metrics + module Dashboard + class Importer + def initialize(dashboard_path, project) + @dashboard_path = dashboard_path.to_s + @project = project + end + + def execute + return false unless Dashboard::Validator.validate(dashboard_hash, project: project, dashboard_path: dashboard_path) + + Dashboard::Importers::PrometheusMetrics.new(dashboard_hash, project: project, dashboard_path: dashboard_path).execute + rescue Gitlab::Config::Loader::FormatError + false + end + + def execute! + Dashboard::Validator.validate!(dashboard_hash, project: project, dashboard_path: dashboard_path) + + Dashboard::Importers::PrometheusMetrics.new(dashboard_hash, project: project, dashboard_path: dashboard_path).execute! + end + + private + + attr_accessor :dashboard_path, :project + + def dashboard_hash + @dashboard_hash ||= begin + raw_dashboard = Dashboard::RepoDashboardFinder.read_dashboard(project, dashboard_path) + return unless raw_dashboard.present? + + ::Gitlab::Config::Loader::Yaml.new(raw_dashboard).load_raw! + end + end + end + end + end +end diff --git a/lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb b/lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb new file mode 100644 index 00000000000..d1490d5d9b6 --- /dev/null +++ b/lib/gitlab/metrics/dashboard/importers/prometheus_metrics.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Gitlab + module Metrics + module Dashboard + module Importers + class PrometheusMetrics + ALLOWED_ATTRIBUTES = %i(title query y_label unit legend group dashboard_path).freeze + + # Takes a JSON schema validated dashboard hash and + # imports metrics to database + def initialize(dashboard_hash, project:, dashboard_path:) + @dashboard_hash = dashboard_hash + @project = project + @dashboard_path = dashboard_path + end + + def execute + import + rescue ActiveRecord::RecordInvalid, ::Gitlab::Metrics::Dashboard::Transformers::TransformerError + false + end + + def execute! + import + end + + private + + attr_reader :dashboard_hash, :project, :dashboard_path + + def import + delete_stale_metrics + create_or_update_metrics + end + + # rubocop: disable CodeReuse/ActiveRecord + def create_or_update_metrics + # TODO: use upsert and worker for callbacks? + prometheus_metrics_attributes.each do |attributes| + prometheus_metric = PrometheusMetric.find_or_initialize_by(attributes.slice(:identifier, :project)) + prometheus_metric.update!(attributes.slice(*ALLOWED_ATTRIBUTES)) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def delete_stale_metrics + identifiers = prometheus_metrics_attributes.map { |metric_attributes| metric_attributes[:identifier] } + + stale_metrics = PrometheusMetric.for_project(project) + .for_dashboard_path(dashboard_path) + .for_group(Enums::PrometheusMetric.groups[:custom]) + .not_identifier(identifiers) + + # TODO: use destroy_all and worker for callbacks? + stale_metrics.each(&:destroy) + end + + def prometheus_metrics_attributes + @prometheus_metrics_attributes ||= begin + Dashboard::Transformers::Yml::V1::PrometheusMetrics.new( + dashboard_hash, + project: project, + dashboard_path: dashboard_path + ).execute + end + end + end + end + end + end +end diff --git a/lib/gitlab/metrics/dashboard/stages/custom_metrics_details_inserter.rb b/lib/gitlab/metrics/dashboard/stages/custom_metrics_details_inserter.rb index cfec027155e..06cfa5cc58e 100644 --- a/lib/gitlab/metrics/dashboard/stages/custom_metrics_details_inserter.rb +++ b/lib/gitlab/metrics/dashboard/stages/custom_metrics_details_inserter.rb @@ -27,7 +27,7 @@ module Gitlab private def custom_group_titles - @custom_group_titles ||= PrometheusMetricEnums.custom_group_details.values.map { |group_details| group_details[:group_title] } + @custom_group_titles ||= Enums::PrometheusMetric.custom_group_details.values.map { |group_details| group_details[:group_title] } end def edit_path(metric) diff --git a/lib/gitlab/metrics/dashboard/transformers/errors.rb b/lib/gitlab/metrics/dashboard/transformers/errors.rb new file mode 100644 index 00000000000..4d94ab098ae --- /dev/null +++ b/lib/gitlab/metrics/dashboard/transformers/errors.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Metrics + module Dashboard + module Transformers + TransformerError = Class.new(StandardError) + + module Errors + class MissingAttribute < TransformerError + def initialize(attribute_name) + super("Missing attribute: '#{attribute_name}'") + end + end + end + end + end + end +end diff --git a/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb b/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb new file mode 100644 index 00000000000..4e46eec17d6 --- /dev/null +++ b/lib/gitlab/metrics/dashboard/transformers/yml/v1/prometheus_metrics.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Gitlab + module Metrics + module Dashboard + module Transformers + module Yml + module V1 + # Takes a JSON schema validated dashboard hash and + # maps it to PrometheusMetric model attributes + class PrometheusMetrics + def initialize(dashboard_hash, project: nil, dashboard_path: nil) + @dashboard_hash = dashboard_hash.with_indifferent_access + @project = project + @dashboard_path = dashboard_path + + @dashboard_hash.default_proc = -> (h, k) { raise Transformers::Errors::MissingAttribute, k.to_s } + end + + def execute + prometheus_metrics = [] + + dashboard_hash[:panel_groups].each do |panel_group| + panel_group[:panels].each do |panel| + panel[:metrics].each do |metric| + prometheus_metrics << { + project: project, + title: panel[:title], + y_label: panel[:y_label], + query: metric[:query_range] || metric[:query], + unit: metric[:unit], + legend: metric[:label], + identifier: metric[:id], + group: Enums::PrometheusMetric.groups[:custom], + common: false, + dashboard_path: dashboard_path + }.compact + end + end + end + + prometheus_metrics + end + + private + + attr_reader :dashboard_hash, :project, :dashboard_path + end + end + end + end + end + end +end diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb index 160ecfb85c9..6dcc73c0f6a 100644 --- a/lib/gitlab/metrics/dashboard/url.rb +++ b/lib/gitlab/metrics/dashboard/url.rb @@ -10,20 +10,23 @@ module Gitlab QUERY_PATTERN = '(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?' ANCHOR_PATTERN = '(?<anchor>\#[a-z0-9_-]+)?' - OPTIONAL_DASH_PATTERN = '(?:/-)?' + DASH_PATTERN = '(?:/-)' - # Matches urls for a metrics dashboard. This could be - # either the /metrics endpoint or the /metrics_dashboard - # endpoint. + # Matches urls for a metrics dashboard. + # This regex needs to match the old metrics URL, the new metrics URL, + # and the dashboard URL (inline_metrics_redactor_filter.rb + # uses this regex to match against the dashboard URL.) # - # EX - https://<host>/<namespace>/<project>/environments/<env_id>/metrics + # EX - Old URL: https://<host>/<namespace>/<project>/environments/<env_id>/metrics + # OR + # New URL: https://<host>/<namespace>/<project>/-/metrics?environment=<env_id> + # OR + # dashboard URL: https://<host>/<namespace>/<project>/environments/<env_id>/metrics_dashboard def metrics_regex strong_memoize(:metrics_regex) do regex_for_project_metrics( %r{ - /environments - /(?<environment>\d+) - /(metrics_dashboard|metrics) + ( #{environment_metrics_regex} ) | ( #{non_environment_metrics_regex} ) }x ) end @@ -36,6 +39,7 @@ module Gitlab strong_memoize(:grafana_regex) do regex_for_project_metrics( %r{ + #{DASH_PATTERN}? /grafana /metrics_dashboard }x @@ -44,16 +48,22 @@ module Gitlab end # Matches dashboard urls for a metric chart embed - # for cluster metrics + # for cluster metrics. + # This regex needs to match the dashboard URL as well, not just the trigger URL. + # The inline_metrics_redactor_filter.rb uses this regex to match against + # the dashboard URL. # # EX - https://<host>/<namespace>/<project>/-/clusters/<cluster_id>/?group=Cluster%20Health&title=Memory%20Usage&y_label=Memory%20(GiB) + # dashboard URL - https://<host>/<namespace>/<project>/-/clusters/<cluster_id>/metrics_dashboard?group=Cluster%20Health&title=Memory%20Usage&y_label=Memory%20(GiB) def clusters_regex strong_memoize(:clusters_regex) do regex_for_project_metrics( %r{ + #{DASH_PATTERN}? /clusters /(?<cluster_id>\d+) /? + ( (/metrics) | ( /metrics_dashboard\.json ) )? }x ) end @@ -67,10 +77,11 @@ module Gitlab strong_memoize(:alert_regex) do regex_for_project_metrics( %r{ + #{DASH_PATTERN}? /prometheus /alerts /(?<alert>\d+) - /metrics_dashboard + /metrics_dashboard(\.json)? }x ) end @@ -95,16 +106,37 @@ module Gitlab private + def environment_metrics_regex + %r{ + #{DASH_PATTERN}? + /environments + /(?<environment>\d+) + /(metrics_dashboard|metrics) + }x + end + + def non_environment_metrics_regex + %r{ + #{DASH_PATTERN} + /metrics + (?= # Lookahead to ensure there is an environment query param + \? + .* + environment=(?<environment>\d+) + .* + ) + }x + end + def regex_for_project_metrics(path_suffix_pattern) %r{ - (?<url> + ^(?<url> #{gitlab_host_pattern} #{project_path_pattern} - #{OPTIONAL_DASH_PATTERN} #{path_suffix_pattern} #{QUERY_PATTERN} #{ANCHOR_PATTERN} - ) + )$ }x end diff --git a/lib/gitlab/metrics/dashboard/validator.rb b/lib/gitlab/metrics/dashboard/validator.rb index 8edd9c397f9..1e8dc059968 100644 --- a/lib/gitlab/metrics/dashboard/validator.rb +++ b/lib/gitlab/metrics/dashboard/validator.rb @@ -4,24 +4,22 @@ module Gitlab module Metrics module Dashboard module Validator - DASHBOARD_SCHEMA_PATH = 'lib/gitlab/metrics/dashboard/validator/schemas/dashboard.json'.freeze + DASHBOARD_SCHEMA_PATH = Rails.root.join(*%w[lib gitlab metrics dashboard validator schemas dashboard.json]).freeze class << self def validate(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil) - errors = _validate(content, schema_path, dashboard_path: dashboard_path, project: project) - errors.empty? + errors(content, schema_path, dashboard_path: dashboard_path, project: project).empty? end def validate!(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil) - errors = _validate(content, schema_path, dashboard_path: dashboard_path, project: project) + errors = errors(content, schema_path, dashboard_path: dashboard_path, project: project) errors.empty? || raise(errors.first) end - private - - def _validate(content, schema_path, dashboard_path: nil, project: nil) - client = Validator::Client.new(content, schema_path, dashboard_path: dashboard_path, project: project) - client.execute + def errors(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil) + Validator::Client + .new(content, schema_path, dashboard_path: dashboard_path, project: project) + .execute end end end diff --git a/lib/gitlab/metrics/dashboard/validator/client.rb b/lib/gitlab/metrics/dashboard/validator/client.rb index c63415abcfc..588c677ca28 100644 --- a/lib/gitlab/metrics/dashboard/validator/client.rb +++ b/lib/gitlab/metrics/dashboard/validator/client.rb @@ -46,7 +46,7 @@ module Gitlab def validate_against_schema schemer.validate(content).map do |error| - Errors::SchemaValidationError.new(error) + ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new(error) end end end diff --git a/lib/gitlab/metrics/dashboard/validator/schemas/panel.json b/lib/gitlab/metrics/dashboard/validator/schemas/panel.json index 011eef53e40..2ae9608036e 100644 --- a/lib/gitlab/metrics/dashboard/validator/schemas/panel.json +++ b/lib/gitlab/metrics/dashboard/validator/schemas/panel.json @@ -4,7 +4,7 @@ "properties": { "type": { "type": "string", - "enum": ["area-chart", "anomaly-chart", "bar", "column", "stacked-column", "single-stat", "heatmap"], + "enum": ["area-chart", "line-chart", "anomaly-chart", "bar", "column", "stacked-column", "single-stat", "heatmap", "gauge"], "default": "area-chart" }, "title": { "type": "string" }, diff --git a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb index 054b4949dd6..36262b09b2d 100644 --- a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb +++ b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb @@ -12,7 +12,11 @@ module Gitlab end def log_filename - File.join(Rails.root, 'log', 'sidekiq_exporter.log') + if settings['log_enabled'] + File.join(Rails.root, 'log', 'sidekiq_exporter.log') + else + File::NULL + end end private diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index ff3fffe7b95..66361529546 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -120,9 +120,6 @@ module Gitlab def self.instrument(type, mod, name) return unless ::Gitlab::Metrics.enabled? - name = name.to_sym - target = type == :instance ? mod : mod.singleton_class - if type == :instance target = mod method_name = "##{name}" @@ -154,6 +151,8 @@ module Gitlab '*args' end + method_visibility = method_visibility_for(target, name) + proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1 def #{name}(#{args_signature}) if trans = Gitlab::Metrics::Instrumentation.transaction @@ -163,11 +162,23 @@ module Gitlab super end end + #{method_visibility} :#{name} EOF target.prepend(proxy_module) end + def self.method_visibility_for(mod, name) + if mod.private_method_defined?(name) + :private + elsif mod.protected_method_defined?(name) + :protected + else + :public + end + end + private_class_method :method_visibility_for + # Small layer of indirection to make it easier to stub out the current # transaction. def self.transaction diff --git a/lib/gitlab/metrics/methods.rb b/lib/gitlab/metrics/methods.rb index 2b5d1c710f6..3100450bc00 100644 --- a/lib/gitlab/metrics/methods.rb +++ b/lib/gitlab/metrics/methods.rb @@ -52,7 +52,7 @@ module Gitlab end def disabled_by_feature(options) - options.with_feature && !::Feature.enabled?(options.with_feature) + options.with_feature && !::Feature.enabled?(options.with_feature, type: :ops) end def build_metric!(type, name, options) diff --git a/lib/gitlab/metrics/samplers/action_cable_sampler.rb b/lib/gitlab/metrics/samplers/action_cable_sampler.rb new file mode 100644 index 00000000000..9f4979fa673 --- /dev/null +++ b/lib/gitlab/metrics/samplers/action_cable_sampler.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Gitlab + module Metrics + module Samplers + class ActionCableSampler < BaseSampler + SAMPLING_INTERVAL_SECONDS = 5 + + def initialize(interval = SAMPLING_INTERVAL_SECONDS, action_cable: ::ActionCable.server) + super(interval) + @action_cable = action_cable + end + + def metrics + @metrics ||= { + active_connections: ::Gitlab::Metrics.gauge( + :action_cable_active_connections, 'Number of ActionCable WS clients currently connected' + ), + pool_min_size: ::Gitlab::Metrics.gauge( + :action_cable_pool_min_size, 'Minimum number of worker threads in ActionCable thread pool' + ), + pool_max_size: ::Gitlab::Metrics.gauge( + :action_cable_pool_max_size, 'Maximum number of worker threads in ActionCable thread pool' + ), + pool_current_size: ::Gitlab::Metrics.gauge( + :action_cable_pool_current_size, 'Current number of worker threads in ActionCable thread pool' + ), + pool_largest_size: ::Gitlab::Metrics.gauge( + :action_cable_pool_largest_size, 'Largest number of worker threads observed so far in ActionCable thread pool' + ), + pool_completed_tasks: ::Gitlab::Metrics.gauge( + :action_cable_pool_tasks_total, 'Total number of tasks executed in ActionCable thread pool' + ), + pool_pending_tasks: ::Gitlab::Metrics.gauge( + :action_cable_pool_pending_tasks, 'Number of tasks waiting to be executed in ActionCable thread pool' + ) + } + end + + def sample + pool = @action_cable.worker_pool.executor + labels = { + server_mode: server_mode + } + + metrics[:active_connections].set(labels, @action_cable.connections.size) + metrics[:pool_min_size].set(labels, pool.min_length) + metrics[:pool_max_size].set(labels, pool.max_length) + metrics[:pool_current_size].set(labels, pool.length) + metrics[:pool_largest_size].set(labels, pool.largest_length) + metrics[:pool_completed_tasks].set(labels, pool.completed_task_count) + metrics[:pool_pending_tasks].set(labels, pool.queue_length) + end + + private + + def server_mode + Gitlab::ActionCable::Config.in_app? ? 'in-app' : 'standalone' + end + end + end + end +end diff --git a/lib/gitlab/metrics/samplers/base_sampler.rb b/lib/gitlab/metrics/samplers/base_sampler.rb index ff3e7be567f..39a49187e45 100644 --- a/lib/gitlab/metrics/samplers/base_sampler.rb +++ b/lib/gitlab/metrics/samplers/base_sampler.rb @@ -21,7 +21,7 @@ module Gitlab def safe_sample sample rescue => e - Rails.logger.warn("#{self.class}: #{e}, stopping") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("#{self.class}: #{e}, stopping") stop end diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb index b5343d5e66a..d295beb59f1 100644 --- a/lib/gitlab/metrics/samplers/puma_sampler.rb +++ b/lib/gitlab/metrics/samplers/puma_sampler.rb @@ -42,7 +42,7 @@ module Gitlab def puma_stats Puma.stats rescue NoMethodError - Rails.logger.info "PumaSampler: stats are not available yet, waiting for Puma to boot" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.info "PumaSampler: stats are not available yet, waiting for Puma to boot" nil end diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb index c0b671abd44..8e6ac7610f2 100644 --- a/lib/gitlab/middleware/multipart.rb +++ b/lib/gitlab/middleware/multipart.rb @@ -29,6 +29,8 @@ module Gitlab module Middleware class Multipart RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS' + JWT_PARAM_SUFFIX = '.gitlab-workhorse-upload' + JWT_PARAM_FIXED_KEY = 'upload' class Handler def initialize(env, message) @@ -57,7 +59,8 @@ module Gitlab yield ensure - @open_files.each(&:close) + @open_files.compact + .each(&:close) end # This function calls itself recursively @@ -122,15 +125,89 @@ module Gitlab def allowed_paths [ + Dir.tmpdir, ::FileUploader.root, - Gitlab.config.uploads.storage_path, - JobArtifactUploader.workhorse_upload_path, - LfsObjectUploader.workhorse_upload_path, + ::Gitlab.config.uploads.storage_path, + ::JobArtifactUploader.workhorse_upload_path, + ::LfsObjectUploader.workhorse_upload_path, File.join(Rails.root, 'public/uploads/tmp') ] + package_allowed_paths end end + # TODO this class is meant to replace Handler when the feature flag + # upload_middleware_jwt_params_handler is removed + class HandlerForJWTParams < Handler + def with_open_files + @rewritten_fields.keys.each do |field| + parsed_field = Rack::Utils.parse_nested_query(field) + raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1 + + key, value = parsed_field.first + if value.nil? # we have a top level param, eg. field = 'foo' and not 'foo[bar]' + raise "invalid field: #{field.inspect}" if field != key + + value = open_file(extract_upload_params_from(@request.params, with_prefix: key)) + @open_files << value + else + value = decorate_params_value(value, @request.params[key]) + end + + update_param(key, value) + end + + yield + ensure + @open_files.compact + .each(&:close) + end + + # This function calls itself recursively + def decorate_params_value(hash_path, value_hash) + unless hash_path.is_a?(Hash) && hash_path.count == 1 + raise "invalid path: #{hash_path.inspect}" + end + + path_key, path_value = hash_path.first + + unless value_hash.is_a?(Hash) && value_hash[path_key] + raise "invalid value hash: #{value_hash.inspect}" + end + + case path_value + when nil + value_hash[path_key] = open_file(extract_upload_params_from(value_hash[path_key])) + @open_files << value_hash[path_key] + value_hash + when Hash + decorate_params_value(path_value, value_hash[path_key]) + value_hash + else + raise "unexpected path value: #{path_value.inspect}" + end + end + + def open_file(params) + ::UploadedFile.from_params_without_field(params, allowed_paths) + end + + private + + def extract_upload_params_from(params, with_prefix: '') + param_key = "#{with_prefix}#{JWT_PARAM_SUFFIX}" + jwt_token = params[param_key] + raise "Empty JWT param: #{param_key}" if jwt_token.blank? + + payload = Gitlab::Workhorse.decode_jwt(jwt_token).first + raise "Invalid JWT payload: not a Hash" unless payload.is_a?(Hash) + + upload_params = payload.fetch(JWT_PARAM_FIXED_KEY, {}) + raise "Empty params for: #{param_key}" if upload_params.empty? + + upload_params + end + end + def initialize(app) @app = app end @@ -139,14 +216,24 @@ module Gitlab encoded_message = env.delete(RACK_ENV_KEY) return @app.call(env) if encoded_message.blank? - message = Gitlab::Workhorse.decode_jwt(encoded_message)[0] + message = ::Gitlab::Workhorse.decode_jwt(encoded_message)[0] - Handler.new(env, message).with_open_files do + handler_class.new(env, message).with_open_files do @app.call(env) end rescue UploadedFile::InvalidPathError => e [400, { 'Content-Type' => 'text/plain' }, e.message] end + + private + + def handler_class + if Feature.enabled?(:upload_middleware_jwt_params_handler) + ::Gitlab::Middleware::Multipart::HandlerForJWTParams + else + ::Gitlab::Middleware::Multipart::Handler + end + end end end end diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb index 6573506e926..cfea4aaddf3 100644 --- a/lib/gitlab/middleware/read_only/controller.rb +++ b/lib/gitlab/middleware/read_only/controller.rb @@ -36,7 +36,7 @@ module Gitlab def call if disallowed_request? && Gitlab::Database.read_only? - Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation') # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.debug('GitLab ReadOnly: preventing possible non read-only operation') if json_request? return [403, { 'Content-Type' => APPLICATION_JSON }, [{ 'message' => ERROR_MESSAGE }.to_json]] diff --git a/lib/gitlab/middleware/same_site_cookies.rb b/lib/gitlab/middleware/same_site_cookies.rb index 45968035e79..37ccc5abb10 100644 --- a/lib/gitlab/middleware/same_site_cookies.rb +++ b/lib/gitlab/middleware/same_site_cookies.rb @@ -30,6 +30,7 @@ module Gitlab set_cookie = headers['Set-Cookie']&.strip return result if set_cookie.blank? || !ssl? + return result if same_site_none_incompatible?(env['HTTP_USER_AGENT']) cookies = set_cookie.split(COOKIE_SEPARATOR) @@ -39,11 +40,11 @@ module Gitlab # Chrome will drop SameSite=None cookies without the Secure # flag. If we remove this middleware, we may need to ensure # that all cookies set this flag. - if ssl? && !(cookie =~ /;\s*secure/i) + unless SECURE_REGEX.match?(cookie) cookie << '; Secure' end - unless cookie =~ /;\s*samesite=/i + unless SAME_SITE_REGEX.match?(cookie) cookie << '; SameSite=None' end end @@ -55,9 +56,93 @@ module Gitlab private + # Taken from https://www.chromium.org/updates/same-site/incompatible-clients + # We use RE2 instead of the browser gem for performance. + IOS_REGEX = RE2('\(iP.+; CPU .*OS (\d+)[_\d]*.*\) AppleWebKit\/') + MACOS_REGEX = RE2('\(Macintosh;.*Mac OS X (\d+)_(\d+)[_\d]*.*\) AppleWebKit\/') + SAFARI_REGEX = RE2('Version\/.* Safari\/') + CHROMIUM_REGEX = RE2('Chrom(e|ium)') + CHROMIUM_VERSION_REGEX = RE2('Chrom[^ \/]+\/(\d+)') + UC_BROWSER_REGEX = RE2('UCBrowser\/') + UC_BROWSER_VERSION_REGEX = RE2('UCBrowser\/(\d+)\.(\d+)\.(\d+)') + + SECURE_REGEX = RE2(';\s*secure', case_sensitive: false) + SAME_SITE_REGEX = RE2(';\s*samesite=', case_sensitive: false) + def ssl? Gitlab.config.gitlab.https end + + def same_site_none_incompatible?(user_agent) + return false if user_agent.blank? + + has_webkit_same_site_bug?(user_agent) || drops_unrecognized_same_site_cookies?(user_agent) + end + + def has_webkit_same_site_bug?(user_agent) + ios_version?(12, user_agent) || + (macos_version?(10, 14, user_agent) && safari?(user_agent)) + end + + def drops_unrecognized_same_site_cookies?(user_agent) + if uc_browser?(user_agent) + return !uc_browser_version_at_least?(12, 13, 2, user_agent) + end + + chromium_based?(user_agent) && chromium_version_between?(51, 66, user_agent) + end + + def ios_version?(major, user_agent) + m = IOS_REGEX.match(user_agent) + + return false if m.nil? + + m[1].to_i == major + end + + def macos_version?(major, minor, user_agent) + m = MACOS_REGEX.match(user_agent) + + return false if m.nil? + + m[1].to_i == major && m[2].to_i == minor + end + + def safari?(user_agent) + SAFARI_REGEX.match?(user_agent) + end + + def chromium_based?(user_agent) + CHROMIUM_REGEX.match?(user_agent) + end + + def chromium_version_between?(from_major, to_major, user_agent) + m = CHROMIUM_VERSION_REGEX.match(user_agent) + + return false if m.nil? + + version = m[1].to_i + version >= from_major && version <= to_major + end + + def uc_browser?(user_agent) + UC_BROWSER_REGEX.match?(user_agent) + end + + def uc_browser_version_at_least?(major, minor, build, user_agent) + m = UC_BROWSER_VERSION_REGEX.match(user_agent) + + return false if m.nil? + + major_version = m[1].to_i + minor_version = m[2].to_i + build_version = m[3].to_i + + return major_version > major if major_version != major + return minor_version > minor if minor_version != minor + + build_version >= build + end end end end diff --git a/lib/gitlab/object_hierarchy.rb b/lib/gitlab/object_hierarchy.rb index 41d80fe9aa6..e18b6d003e0 100644 --- a/lib/gitlab/object_hierarchy.rb +++ b/lib/gitlab/object_hierarchy.rb @@ -7,18 +7,19 @@ module Gitlab class ObjectHierarchy DEPTH_COLUMN = :depth - attr_reader :ancestors_base, :descendants_base, :model + attr_reader :ancestors_base, :descendants_base, :model, :options # ancestors_base - An instance of ActiveRecord::Relation for which to # get parent objects. # descendants_base - An instance of ActiveRecord::Relation for which to # get child objects. If omitted, ancestors_base is used. - def initialize(ancestors_base, descendants_base = ancestors_base) + def initialize(ancestors_base, descendants_base = ancestors_base, options: {}) raise ArgumentError.new("Model of ancestors_base does not match model of descendants_base") if ancestors_base.model != descendants_base.model @ancestors_base = ancestors_base @descendants_base = descendants_base @model = ancestors_base.model + @options = options end # Returns the set of descendants of a given relation, but excluding the given @@ -133,8 +134,8 @@ module Gitlab # Recursively get all the ancestors of the base set. parent_query = model - .from([objects_table, cte.table]) - .where(objects_table[:id].eq(cte.table[:parent_id])) + .from(from_tables(cte)) + .where(ancestor_conditions(cte)) .except(:order) if hierarchy_order @@ -148,7 +149,7 @@ module Gitlab ).where(cte.table[:tree_cycle].eq(false)) end - parent_query = parent_query.where(cte.table[:parent_id].not_eq(stop_id)) if stop_id + parent_query = parent_query.where(parent_id_column(cte).not_eq(stop_id)) if stop_id cte << parent_query cte @@ -166,8 +167,8 @@ module Gitlab # Recursively get all the descendants of the base set. descendants_query = model - .from([objects_table, cte.table]) - .where(objects_table[:parent_id].eq(cte.table[:id])) + .from(from_tables(cte)) + .where(descendant_conditions(cte)) .except(:order) if with_depth @@ -190,6 +191,22 @@ module Gitlab model.arel_table end + def parent_id_column(cte) + cte.table[:parent_id] + end + + def from_tables(cte) + [objects_table, cte.table] + end + + def ancestor_conditions(cte) + objects_table[:id].eq(cte.table[:parent_id]) + end + + def descendant_conditions(cte) + objects_table[:parent_id].eq(cte.table[:id]) + end + def read_only(relation) # relations using a CTE are not safe to use with update_all as it will # throw away the CTE, hence we mark them as read-only. diff --git a/lib/gitlab/pages/settings.rb b/lib/gitlab/pages/settings.rb index e3dbeee7b13..8650a80a85e 100644 --- a/lib/gitlab/pages/settings.rb +++ b/lib/gitlab/pages/settings.rb @@ -6,12 +6,8 @@ module Gitlab DiskAccessDenied = Class.new(StandardError) def path - if ::Gitlab::Runtime.web_server? && ENV['GITLAB_PAGES_DENY_DISK_ACCESS'] == '1' - begin - raise DiskAccessDenied - rescue DiskAccessDenied => ex - ::Gitlab::ErrorTracking.track_exception(ex) - end + if ::Gitlab::Runtime.web_server? && !::Gitlab::Runtime.test_suite? + raise DiskAccessDenied end super diff --git a/lib/gitlab/pages_transfer.rb b/lib/gitlab/pages_transfer.rb index a70dc826f97..8ae0ec5a78a 100644 --- a/lib/gitlab/pages_transfer.rb +++ b/lib/gitlab/pages_transfer.rb @@ -1,7 +1,26 @@ # frozen_string_literal: true +# To make a call happen in a new Sidekiq job, add `.async` before the call. For +# instance: +# +# PagesTransfer.new.async.move_namespace(...) +# module Gitlab class PagesTransfer < ProjectTransfer + class Async + METHODS = %w[move_namespace move_project rename_project rename_namespace].freeze + + METHODS.each do |meth| + define_method meth do |*args| + PagesTransferWorker.perform_async(meth, args) + end + end + end + + def async + @async ||= Async.new + end + def root_dir Gitlab.config.pages.path end diff --git a/lib/gitlab/project_authorizations.rb b/lib/gitlab/project_authorizations.rb index ff90a009b2e..23e380b3cf1 100644 --- a/lib/gitlab/project_authorizations.rb +++ b/lib/gitlab/project_authorizations.rb @@ -99,6 +99,7 @@ module Gitlab .and(members[:source_type].eq('Namespace')) .and(members[:requested_at].eq(nil)) .and(members[:user_id].eq(user.id)) + .and(members[:access_level].gt(Gitlab::Access::MINIMAL_ACCESS)) Arel::Nodes::OuterJoin.new(members, Arel::Nodes::On.new(cond)) end @@ -119,6 +120,7 @@ module Gitlab .and(members[:source_type].eq('Namespace')) .and(members[:requested_at].eq(nil)) .and(members[:user_id].eq(user.id)) + .and(members[:access_level].gt(Gitlab::Access::MINIMAL_ACCESS)) Arel::Nodes::InnerJoin.new(members, Arel::Nodes::On.new(cond)) end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index f8141278e48..ded8d4ade3f 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -4,11 +4,11 @@ module Gitlab class ProjectSearchResults < SearchResults attr_reader :project, :repository_ref - def initialize(current_user, project, query, repository_ref = nil) - @current_user = current_user + def initialize(current_user, query, project:, repository_ref: nil, filters: {}) @project = project @repository_ref = repository_ref.presence - @query = query + + super(current_user, query, [project], filters: filters) end def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil) diff --git a/lib/gitlab/prometheus/internal.rb b/lib/gitlab/prometheus/internal.rb index d59352119ba..c2f4035821e 100644 --- a/lib/gitlab/prometheus/internal.rb +++ b/lib/gitlab/prometheus/internal.rb @@ -25,6 +25,10 @@ module Gitlab end end + def self.server_address + uri&.strip&.sub(/^http[s]?:\/\//, '') + end + def self.listen_address Gitlab.config.prometheus.listen_address.to_s if Gitlab.config.prometheus rescue Settingslogic::MissingSetting diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb index 56e1154a672..965349ad711 100644 --- a/lib/gitlab/prometheus_client.rb +++ b/lib/gitlab/prometheus_client.rb @@ -42,6 +42,15 @@ module Gitlab response_body == HEALTHY_RESPONSE end + def ready? + response = get(ready_url, {}) + + # From Prometheus docs: This endpoint returns 200 when Prometheus is ready to serve traffic (i.e. respond to queries). + response.code == 200 + rescue => e + raise PrometheusClient::UnexpectedResponseError, "#{e.message}" + end + def proxy(type, args) path = api_path(type) get(path, args) @@ -103,7 +112,11 @@ module Gitlab end def health_url - [api_url, '-/healthy'].join('/') + "#{api_url}/-/healthy" + end + + def ready_url + "#{api_url}/-/ready" end private diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb index cd07122ffd9..dd7a27ead01 100644 --- a/lib/gitlab/quick_actions/extractor.rb +++ b/lib/gitlab/quick_actions/extractor.rb @@ -9,6 +9,62 @@ module Gitlab # extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels]) # ``` class Extractor + CODE_REGEX = %r{ + (?<code> + # Code blocks: + # ``` + # Anything, including `/cmd arg` which are ignored by this filter + # ``` + + ^``` + .+? + \n```$ + ) + }mix.freeze + + INLINE_CODE_REGEX = %r{ + (?<inline_code> + # Inline code on separate rows: + # ` + # Anything, including `/cmd arg` which are ignored by this filter + # ` + + ^.*`\n* + .+? + \n*`$ + ) + }mix.freeze + + HTML_BLOCK_REGEX = %r{ + (?<html> + # HTML block: + # <tag> + # Anything, including `/cmd arg` which are ignored by this filter + # </tag> + + ^<[^>]+?>\n + .+? + \n<\/[^>]+?>$ + ) + }mix.freeze + + QUOTE_BLOCK_REGEX = %r{ + (?<html> + # Quote block: + # >>> + # Anything, including `/cmd arg` which are ignored by this filter + # >>> + + ^>>> + .+? + \n>>>$ + ) + }mix.freeze + + EXCLUSION_REGEX = %r{ + #{CODE_REGEX} | #{INLINE_CODE_REGEX} | #{HTML_BLOCK_REGEX} | #{QUOTE_BLOCK_REGEX} + }mix.freeze + attr_reader :command_definitions def initialize(command_definitions) @@ -35,9 +91,7 @@ module Gitlab def extract_commands(content, only: nil) return [content, []] unless content - content, commands = perform_regex(content, only: only) - - perform_substitutions(content, commands) + perform_regex(content, only: only) end # Encloses quick action commands into code span markdown @@ -55,13 +109,19 @@ module Gitlab private def perform_regex(content, only: nil, redact: false) - commands = [] - content = content.dup + names = command_names(limit_to_commands: only).map(&:to_s) + sub_names = substitution_names.map(&:to_s) + commands = [] + content = content.dup content.delete!("\r") - names = command_names(limit_to_commands: only).map(&:to_s) - content.gsub!(commands_regex(names: names)) do - command, output = process_commands($~, redact) + content.gsub!(commands_regex(names: names, sub_names: sub_names)) do + command, output = if $~[:substitution] + process_substitutions($~) + else + process_commands($~, redact) + end + commands << command output end @@ -86,6 +146,21 @@ module Gitlab [command, output] end + def process_substitutions(matched_text) + output = matched_text[0] + command = [] + + if matched_text[:substitution] + cmd = matched_text[:substitution].downcase + command = [cmd, matched_text[:arg]].reject(&:blank?) + + substitution = substitution_definitions.find { |definition| definition.all_names.include?(cmd.to_sym) } + output = substitution.perform_substitution(self, output) if substitution + end + + [command, output] + end + # Builds a regular expression to match known commands. # First match group captures the command name and # second match group captures its arguments. @@ -93,51 +168,9 @@ module Gitlab # It looks something like: # # /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/ - def commands_regex(names:) + def commands_regex(names:, sub_names:) @commands_regex[names] ||= %r{ - (?<code> - # Code blocks: - # ``` - # Anything, including `/cmd arg` which are ignored by this filter - # ``` - - ^``` - .+? - \n```$ - ) - | - (?<inline_code> - # Inline code on separate rows: - # ` - # Anything, including `/cmd arg` which are ignored by this filter - # ` - - ^.*`\n* - .+? - \n*`$ - ) - | - (?<html> - # HTML block: - # <tag> - # Anything, including `/cmd arg` which are ignored by this filter - # </tag> - - ^<[^>]+?>\n - .+? - \n<\/[^>]+?>$ - ) - | - (?<html> - # Quote block: - # >>> - # Anything, including `/cmd arg` which are ignored by this filter - # >>> - - ^>>> - .+? - \n>>>$ - ) + #{EXCLUSION_REGEX} | (?: # Command not in a blockquote, blockcode, or HTML tag: @@ -151,32 +184,19 @@ module Gitlab )? (?:\s*\n|$) ) - }mix - end - - def perform_substitutions(content, commands) - return unless content - - substitution_definitions = self.command_definitions.select do |definition| - definition.is_a?(Gitlab::QuickActions::SubstitutionDefinition) - end - - substitution_definitions.each do |substitution| - regex = commands_regex(names: substitution.all_names) - content = content.gsub(regex) do |text| - if $~[:cmd] - command = [substitution.name.to_s] - command << $~[:arg] if $~[:arg].present? - commands << command - - substitution.perform_substitution(self, text) - else - text - end - end - end + | + (?: + # Substitution not in a blockquote, blockcode, or HTML tag: - [content, commands] + ^\/ + (?<substitution>#{Regexp.new(Regexp.union(sub_names).source, Regexp::IGNORECASE)}) + (?: + [ ] + (?<arg>[^\n]*) + )? + (?:\s*\n|$) + ) + }mix end def command_names(limit_to_commands:) @@ -190,6 +210,17 @@ module Gitlab command.all_names end.compact end + + def substitution_names + substitution_definitions.flat_map { |command| command.all_names } + .compact + end + + def substitution_definitions + @substition_definitions ||= command_definitions.select do |command| + command.is_a?(Gitlab::QuickActions::SubstitutionDefinition) + end + end end end end diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb index aff3ed53734..6607c73a5c3 100644 --- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb +++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb @@ -91,6 +91,7 @@ module Gitlab params '%"milestone"' types Issue, MergeRequest condition do + quick_action_target.supports_milestone? && current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) && find_milestones(project, state: 'active').any? end @@ -113,6 +114,7 @@ module Gitlab condition do quick_action_target.persisted? && quick_action_target.milestone_id? && + quick_action_target.supports_milestone? && current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) end command :remove_milestone do diff --git a/lib/gitlab/quick_actions/relate_actions.rb b/lib/gitlab/quick_actions/relate_actions.rb new file mode 100644 index 00000000000..95f71214667 --- /dev/null +++ b/lib/gitlab/quick_actions/relate_actions.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module QuickActions + module RelateActions + extend ActiveSupport::Concern + include ::Gitlab::QuickActions::Dsl + + included do + desc _('Mark this issue as related to another issue') + explanation do |related_reference| + _('Marks this issue as related to %{issue_ref}.') % { issue_ref: related_reference } + end + execution_message do |related_reference| + _('Marked this issue as related to %{issue_ref}.') % { issue_ref: related_reference } + end + params '#issue' + types Issue + condition do + quick_action_target.persisted? && + current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target) + end + command :relate do |related_param| + IssueLinks::CreateService.new(quick_action_target, current_user, { issuable_references: [related_param] }).execute + end + end + end + end +end diff --git a/lib/gitlab/quick_actions/substitution_definition.rb b/lib/gitlab/quick_actions/substitution_definition.rb index cd4d202e8d0..24b4e3c62b3 100644 --- a/lib/gitlab/quick_actions/substitution_definition.rb +++ b/lib/gitlab/quick_actions/substitution_definition.rb @@ -9,10 +9,6 @@ module Gitlab true end - def match(content) - content.match %r{^/#{all_names.join('|')}(?![\S]) ?(.*)$} - end - def perform_substitution(context, content) return unless content diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb index 8ab53700932..2848c9f0b59 100644 --- a/lib/gitlab/redis/shared_state.rb +++ b/lib/gitlab/redis/shared_state.rb @@ -9,7 +9,7 @@ module Gitlab SESSION_NAMESPACE = 'session:gitlab' USER_SESSIONS_NAMESPACE = 'session:user:gitlab' USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab' - IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab' + IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab2' DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382' REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE' diff --git a/lib/gitlab/reference_counter.rb b/lib/gitlab/reference_counter.rb index 5fdfa5e75ed..c2fa2e1330a 100644 --- a/lib/gitlab/reference_counter.rb +++ b/lib/gitlab/reference_counter.rb @@ -51,10 +51,8 @@ module Gitlab redis_cmd do |redis| current_value = redis.decr(key) if current_value < 0 - # rubocop:disable Gitlab/RailsLogger - Rails.logger.warn("Reference counter for #{gl_repository} decreased" \ + Gitlab::AppLogger.warn("Reference counter for #{gl_repository} decreased" \ " when its value was less than 1. Resetting the counter.") - # rubocop:enable Gitlab/RailsLogger redis.del(key) end end @@ -87,7 +85,7 @@ module Gitlab true rescue => e - Rails.logger.warn("GitLab: An unexpected error occurred in writing to Redis: #{e}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("GitLab: An unexpected error occurred in writing to Redis: #{e}") false end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 2d625737e05..8e23ac6aca5 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -46,6 +46,21 @@ module Gitlab maven_app_name_regex end + def pypi_version_regex + # See the official regex: https://github.com/pypa/packaging/blob/16.7/packaging/version.py#L159 + + @pypi_version_regex ||= %r{ + \A(?: + v? + (?:([0-9]+)!)? (?# epoch) + ([0-9]+(?:\.[0-9]+)*) (?# release segment) + ([-_\.]?((a|b|c|rc|alpha|beta|pre|preview))[-_\.]?([0-9]+)?)? (?# pre-release) + ((?:-([0-9]+))|(?:[-_\.]?(post|rev|r)[-_\.]?([0-9]+)?))? (?# post release) + ([-_\.]?(dev)[-_\.]?([0-9]+)?)? (?# dev release) + (?:\+([a-z0-9]+(?:[-_\.][a-z0-9]+)*))? (?# local version) + )\z}xi.freeze + end + def unbounded_semver_regex # See the official regex: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string @@ -84,6 +99,10 @@ module Gitlab \b (?# word boundary) /ix.freeze end + + def generic_package_version_regex + /\A\d+\.\d+\.\d+\z/ + end end extend self @@ -102,7 +121,11 @@ module Gitlab end def group_name_regex - @group_name_regex ||= /\A[\p{Alnum}\u{00A9}-\u{1f9ff}_][\p{Alnum}\p{Pd}\u{00A9}-\u{1f9ff}_()\. ]*\z/.freeze + @group_name_regex ||= /\A#{group_name_regex_chars}\z/.freeze + end + + def group_name_regex_chars + @group_name_regex_chars ||= /[\p{Alnum}\u{00A9}-\u{1f9ff}_][\p{Alnum}\p{Pd}\u{00A9}-\u{1f9ff}_()\. ]*/.freeze end def group_name_regex_message @@ -269,7 +292,14 @@ module Gitlab def base64_regex @base64_regex ||= /(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?/.freeze end + + def feature_flag_regex + /\A[a-z]([-_a-z0-9]*[a-z0-9])?\z/ + end + + def feature_flag_regex_message + "can contain only lowercase letters, digits, '_' and '-'. " \ + "Must start with a letter, and cannot end with '-' or '_'" + end end end - -Gitlab::Regex.prepend_if_ee('EE::Gitlab::Regex') diff --git a/lib/gitlab/relative_positioning.rb b/lib/gitlab/relative_positioning.rb new file mode 100644 index 00000000000..b5a923f0824 --- /dev/null +++ b/lib/gitlab/relative_positioning.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module RelativePositioning + STEPS = 10 + IDEAL_DISTANCE = 2**(STEPS - 1) + 1 + + MIN_POSITION = Gitlab::Database::MIN_INT_VALUE + START_POSITION = 0 + MAX_POSITION = Gitlab::Database::MAX_INT_VALUE + + MAX_GAP = IDEAL_DISTANCE * 2 + MIN_GAP = 2 + + NoSpaceLeft = Class.new(StandardError) + end +end diff --git a/lib/gitlab/relative_positioning/gap.rb b/lib/gitlab/relative_positioning/gap.rb new file mode 100644 index 00000000000..ab894141a60 --- /dev/null +++ b/lib/gitlab/relative_positioning/gap.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +# +module Gitlab + module RelativePositioning + class Gap + attr_reader :start_pos, :end_pos + + def initialize(start_pos, end_pos) + @start_pos, @end_pos = start_pos, end_pos + end + + def ==(other) + other.is_a?(self.class) && other.start_pos == start_pos && other.end_pos == end_pos + end + + def delta + ((start_pos - end_pos) / 2.0).abs.ceil.clamp(0, RelativePositioning::IDEAL_DISTANCE) + end + end + end +end diff --git a/lib/gitlab/relative_positioning/item_context.rb b/lib/gitlab/relative_positioning/item_context.rb new file mode 100644 index 00000000000..cd03a347355 --- /dev/null +++ b/lib/gitlab/relative_positioning/item_context.rb @@ -0,0 +1,259 @@ +# frozen_string_literal: true + +module Gitlab + module RelativePositioning + # This class is API private - it should not be explicitly instantiated + # outside of tests + # rubocop: disable CodeReuse/ActiveRecord + class ItemContext + include Gitlab::Utils::StrongMemoize + + attr_reader :object, :model_class, :range + attr_accessor :ignoring + + def initialize(object, range, ignoring: nil) + @object = object + @range = range + @model_class = object.class + @ignoring = ignoring + end + + def ==(other) + other.is_a?(self.class) && other.object == object && other.range == range && other.ignoring == ignoring + end + + def positioned? + relative_position.present? + end + + def min_relative_position + strong_memoize(:min_relative_position) { calculate_relative_position('MIN') } + end + + def max_relative_position + strong_memoize(:max_relative_position) { calculate_relative_position('MAX') } + end + + def prev_relative_position + calculate_relative_position('MAX') { |r| nextify(r, false) } if object.relative_position + end + + def next_relative_position + calculate_relative_position('MIN') { |r| nextify(r) } if object.relative_position + end + + def nextify(relation, gt = true) + if gt + relation.where("relative_position > ?", relative_position) + else + relation.where("relative_position < ?", relative_position) + end + end + + def relative_siblings(relation = scoped_items) + object.exclude_self(relation) + end + + # Handles the possibility that the position is already occupied by a sibling + def place_at_position(position, lhs) + current_occupant = relative_siblings.find_by(relative_position: position) + + if current_occupant.present? + Mover.new(position, range).move(object, lhs.object, current_occupant) + else + object.relative_position = position + end + end + + def lhs_neighbour + scoped_items + .where('relative_position < ?', relative_position) + .reorder(relative_position: :desc) + .first + .then { |x| neighbour(x) } + end + + def rhs_neighbour + scoped_items + .where('relative_position > ?', relative_position) + .reorder(relative_position: :asc) + .first + .then { |x| neighbour(x) } + end + + def neighbour(item) + return unless item.present? + + self.class.new(item, range, ignoring: ignoring) + end + + def scoped_items + r = model_class.relative_positioning_query_base(object) + r = object.exclude_self(r, excluded: ignoring) if ignoring.present? + r + end + + def calculate_relative_position(calculation) + # When calculating across projects, this is much more efficient than + # MAX(relative_position) without the GROUP BY, due to index usage: + # https://gitlab.com/gitlab-org/gitlab-foss/issues/54276#note_119340977 + relation = scoped_items + .order(Gitlab::Database.nulls_last_order('position', 'DESC')) + .group(grouping_column) + .limit(1) + + relation = yield relation if block_given? + + relation + .pluck(grouping_column, Arel.sql("#{calculation}(relative_position) AS position")) + .first&.last + end + + def grouping_column + model_class.relative_positioning_parent_column + end + + def max_sibling + sib = relative_siblings + .order(Gitlab::Database.nulls_last_order('relative_position', 'DESC')) + .first + + neighbour(sib) + end + + def min_sibling + sib = relative_siblings + .order(Gitlab::Database.nulls_last_order('relative_position', 'ASC')) + .first + + neighbour(sib) + end + + def shift_left + move_sequence_before(true) + object.reset + end + + def shift_right + move_sequence_after(true) + object.reset + end + + def create_space_left + find_next_gap_before.tap { |gap| move_sequence_before(false, next_gap: gap) } + end + + def create_space_right + find_next_gap_after.tap { |gap| move_sequence_after(false, next_gap: gap) } + end + + def find_next_gap_before + items_with_next_pos = scoped_items + .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos') + .where('relative_position <= ?', relative_position) + .order(relative_position: :desc) + + find_next_gap(items_with_next_pos, range.first) + end + + def find_next_gap_after + items_with_next_pos = scoped_items + .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos') + .where('relative_position >= ?', relative_position) + .order(:relative_position) + + find_next_gap(items_with_next_pos, range.last) + end + + def find_next_gap(items_with_next_pos, default_end) + gap = model_class + .from(items_with_next_pos, :items) + .where('next_pos IS NULL OR ABS(pos::bigint - next_pos::bigint) >= ?', MIN_GAP) + .limit(1) + .pluck(:pos, :next_pos) + .first + + return if gap.nil? || gap.first == default_end + + Gap.new(gap.first, gap.second || default_end) + end + + def relative_position + object.relative_position + end + + private + + # Moves the sequence before the current item to the middle of the next gap + # For example, we have + # + # 5 . . . . . 11 12 13 14 [15] 16 . 17 + # ----------- + # + # This moves the sequence [11 12 13 14] to [8 9 10 11], so we have: + # + # 5 . . 8 9 10 11 . . . [15] 16 . 17 + # --------- + # + # Creating a gap to the left of the current item. We can understand this as + # dividing the 5 spaces between 5 and 11 into two smaller gaps of 2 and 3. + # + # If `include_self` is true, the current item will also be moved, creating a + # gap to the right of the current item: + # + # 5 . . 8 9 10 11 [14] . . . 16 . 17 + # -------------- + # + # As an optimization, the gap can be precalculated and passed to this method. + # + # @api private + # @raises NoSpaceLeft if the sequence cannot be moved + def move_sequence_before(include_self = false, next_gap: find_next_gap_before) + raise NoSpaceLeft unless next_gap.present? + + delta = next_gap.delta + + move_sequence(next_gap.start_pos, relative_position, -delta, include_self) + end + + # Moves the sequence after the current item to the middle of the next gap + # For example, we have: + # + # 8 . 10 [11] 12 13 14 15 . . . . . 21 + # ----------- + # + # This moves the sequence [12 13 14 15] to [15 16 17 18], so we have: + # + # 8 . 10 [11] . . . 15 16 17 18 . . 21 + # ----------- + # + # Creating a gap to the right of the current item. We can understand this as + # dividing the 5 spaces between 15 and 21 into two smaller gaps of 3 and 2. + # + # If `include_self` is true, the current item will also be moved, creating a + # gap to the left of the current item: + # + # 8 . 10 . . . [14] 15 16 17 18 . . 21 + # ---------------- + # + # As an optimization, the gap can be precalculated and passed to this method. + # + # @api private + # @raises NoSpaceLeft if the sequence cannot be moved + def move_sequence_after(include_self = false, next_gap: find_next_gap_after) + raise NoSpaceLeft unless next_gap.present? + + delta = next_gap.delta + + move_sequence(relative_position, next_gap.start_pos, delta, include_self) + end + + def move_sequence(start_pos, end_pos, delta, include_self = false) + relation = include_self ? scoped_items : relative_siblings + + object.update_relative_siblings(relation, (start_pos..end_pos), delta) + end + end + # rubocop: enable CodeReuse/ActiveRecord + end +end diff --git a/lib/gitlab/relative_positioning/mover.rb b/lib/gitlab/relative_positioning/mover.rb new file mode 100644 index 00000000000..9d891bfbe3b --- /dev/null +++ b/lib/gitlab/relative_positioning/mover.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +module Gitlab + module RelativePositioning + class Mover + attr_reader :range, :start_position + + def initialize(start, range) + @range = range + @start_position = start + end + + def move_to_end(object) + focus = context(object, ignoring: object) + max_pos = focus.max_relative_position + + move_to_range_end(focus, max_pos) + end + + def move_to_start(object) + focus = context(object, ignoring: object) + min_pos = focus.min_relative_position + + move_to_range_start(focus, min_pos) + end + + def move(object, first, last) + raise ArgumentError, 'object is required' unless object + + lhs = context(first, ignoring: object) + rhs = context(last, ignoring: object) + focus = context(object) + range = RelativePositioning.range(lhs, rhs) + + if range.cover?(focus) + # Moving a object already within a range is a no-op + elsif range.open_on_left? + move_to_range_start(focus, range.rhs.relative_position) + elsif range.open_on_right? + move_to_range_end(focus, range.lhs.relative_position) + else + pos_left, pos_right = create_space_between(range) + desired_position = position_between(pos_left, pos_right) + focus.place_at_position(desired_position, range.lhs) + end + end + + def context(object, ignoring: nil) + return unless object + + ItemContext.new(object, range, ignoring: ignoring) + end + + private + + def gap_too_small?(pos_a, pos_b) + return false unless pos_a && pos_b + + (pos_a - pos_b).abs < MIN_GAP + end + + def move_to_range_end(context, max_pos) + range_end = range.last + 1 + + new_pos = if max_pos.nil? + start_position + elsif gap_too_small?(max_pos, range_end) + max = context.max_sibling + max.ignoring = context.object + max.shift_left + position_between(max.relative_position, range_end) + else + position_between(max_pos, range_end) + end + + context.object.relative_position = new_pos + end + + def move_to_range_start(context, min_pos) + range_end = range.first - 1 + + new_pos = if min_pos.nil? + start_position + elsif gap_too_small?(min_pos, range_end) + sib = context.min_sibling + sib.ignoring = context.object + sib.shift_right + position_between(sib.relative_position, range_end) + else + position_between(min_pos, range_end) + end + + context.object.relative_position = new_pos + end + + def create_space_between(range) + pos_left = range.lhs&.relative_position + pos_right = range.rhs&.relative_position + + return [pos_left, pos_right] unless gap_too_small?(pos_left, pos_right) + + gap = range.rhs.create_space_left + [pos_left - gap.delta, pos_right] + rescue NoSpaceLeft + gap = range.lhs.create_space_right + [pos_left, pos_right + gap.delta] + end + + # This method takes two integer values (positions) and + # calculates the position between them. The range is huge as + # the maximum integer value is 2147483647. + # + # We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION]. + # + # Then we handle one of three cases: + # - If the gap is too small, we raise NoSpaceLeft + # - If the gap is larger than MAX_GAP, we place the new position at most + # IDEAL_DISTANCE from the edge of the gap. + # - otherwise we place the new position at the midpoint. + # + # The new position will always satisfy: pos_before <= midpoint <= pos_after + # + # As a precondition, the gap between pos_before and pos_after MUST be >= 2. + # If the gap is too small, NoSpaceLeft is raised. + # + # @raises NoSpaceLeft + def position_between(pos_before, pos_after) + pos_before ||= range.first + pos_after ||= range.last + + pos_before, pos_after = [pos_before, pos_after].sort + + gap_width = pos_after - pos_before + + if gap_too_small?(pos_before, pos_after) + raise NoSpaceLeft + elsif gap_width > MAX_GAP + if pos_before <= range.first + pos_after - IDEAL_DISTANCE + elsif pos_after >= range.last + pos_before + IDEAL_DISTANCE + else + midpoint(pos_before, pos_after) + end + else + midpoint(pos_before, pos_after) + end + end + + def midpoint(lower_bound, upper_bound) + ((lower_bound + upper_bound) / 2.0).ceil.clamp(lower_bound, upper_bound - 1) + end + end + end +end diff --git a/lib/gitlab/relative_positioning/range.rb b/lib/gitlab/relative_positioning/range.rb new file mode 100644 index 00000000000..174d5ef4b35 --- /dev/null +++ b/lib/gitlab/relative_positioning/range.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Gitlab + module RelativePositioning + IllegalRange = Class.new(ArgumentError) + + class Range + attr_reader :lhs, :rhs + + def open_on_left? + lhs.nil? + end + + def open_on_right? + rhs.nil? + end + + def cover?(item_context) + return false unless item_context + return false unless item_context.positioned? + return true if item_context.object == lhs&.object + return true if item_context.object == rhs&.object + + pos = item_context.relative_position + + return lhs.relative_position < pos if open_on_right? + return pos < rhs.relative_position if open_on_left? + + lhs.relative_position < pos && pos < rhs.relative_position + end + + def ==(other) + other.is_a?(RelativePositioning::Range) && lhs == other.lhs && rhs == other.rhs + end + end + + def self.range(lhs, rhs) + if lhs && rhs + ClosedRange.new(lhs, rhs) + elsif lhs + StartingFrom.new(lhs) + elsif rhs + EndingAt.new(rhs) + else + raise IllegalRange, 'One of rhs or lhs must be provided' unless lhs && rhs + end + end + + class ClosedRange < RelativePositioning::Range + def initialize(lhs, rhs) + @lhs, @rhs = lhs, rhs + raise IllegalRange, 'Either lhs or rhs is missing' unless lhs && rhs + raise IllegalRange, 'lhs and rhs cannot be the same object' if lhs == rhs + end + end + + class StartingFrom < RelativePositioning::Range + include Gitlab::Utils::StrongMemoize + + def initialize(lhs) + @lhs = lhs + raise IllegalRange, 'lhs is required' unless lhs + end + + def rhs + strong_memoize(:rhs) { lhs.rhs_neighbour } + end + end + + class EndingAt < RelativePositioning::Range + include Gitlab::Utils::StrongMemoize + + def initialize(rhs) + @rhs = rhs + raise IllegalRange, 'rhs is required' unless rhs + end + + def lhs + strong_memoize(:lhs) { rhs.lhs_neighbour } + end + end + end +end diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb index f6a5c6ed754..eb7c9bccf96 100644 --- a/lib/gitlab/repository_cache_adapter.rb +++ b/lib/gitlab/repository_cache_adapter.rb @@ -218,7 +218,7 @@ module Gitlab def expire_method_caches(methods) methods.each do |name| unless cached_methods.include?(name.to_sym) - Rails.logger.error "Requested to expire non-existent method '#{name}' for Repository" # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error "Requested to expire non-existent method '#{name}' for Repository" next end diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb index 1e2d86b7ad2..69c1688767c 100644 --- a/lib/gitlab/repository_set_cache.rb +++ b/lib/gitlab/repository_set_cache.rb @@ -22,7 +22,7 @@ module Gitlab with do |redis| redis.multi do - redis.del(full_key) + redis.unlink(full_key) # Splitting into groups of 1000 prevents us from creating a too-long # Redis command diff --git a/lib/gitlab/request_profiler.rb b/lib/gitlab/request_profiler.rb index dd1482da40d..541d505e735 100644 --- a/lib/gitlab/request_profiler.rb +++ b/lib/gitlab/request_profiler.rb @@ -11,7 +11,7 @@ module Gitlab Profile.new(File.basename(path)) end.select(&:valid?) end - module_function :all + module_function :all # rubocop: disable Style/AccessModifierDeclarations def find(name) file_path = File.join(PROFILES_DIR, name) @@ -19,18 +19,18 @@ module Gitlab Profile.new(name) end - module_function :find + module_function :find # rubocop: disable Style/AccessModifierDeclarations def profile_token Rails.cache.fetch('profile-token') do Devise.friendly_token end end - module_function :profile_token + module_function :profile_token # rubocop: disable Style/AccessModifierDeclarations def remove_all_profiles FileUtils.rm_rf(PROFILES_DIR) end - module_function :remove_all_profiles + module_function :remove_all_profiles # rubocop: disable Style/AccessModifierDeclarations end end diff --git a/lib/gitlab/robots_txt.rb b/lib/gitlab/robots_txt.rb new file mode 100644 index 00000000000..2f395548770 --- /dev/null +++ b/lib/gitlab/robots_txt.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module RobotsTxt + def self.disallowed?(path) + parsed_robots_txt.disallowed?(path) + end + + def self.parsed_robots_txt + @parsed_robots_txt ||= Parser.new(robots_txt) + end + + def self.robots_txt + File.read(Rails.root.join('public', 'robots.txt')) + end + end +end diff --git a/lib/gitlab/robots_txt/parser.rb b/lib/gitlab/robots_txt/parser.rb new file mode 100644 index 00000000000..b9a3837e468 --- /dev/null +++ b/lib/gitlab/robots_txt/parser.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Gitlab + module RobotsTxt + class Parser + attr_reader :disallow_rules + + def initialize(content) + @raw_content = content + + @disallow_rules = parse_raw_content! + end + + def disallowed?(path) + disallow_rules.any? { |rule| path =~ rule } + end + + private + + # This parser is very basic as it only knows about `Disallow:` lines, + # and simply ignores all other lines. + # + # Order of predecence, 'Allow:`, etc are ignored for now. + def parse_raw_content! + @raw_content.each_line.map do |line| + if line.start_with?('Disallow:') + value = line.sub('Disallow:', '').strip + value = Regexp.escape(value).gsub('\*', '.*') + Regexp.new("^#{value}") + else + nil + end + end.compact + end + end + end +end diff --git a/lib/gitlab/sanitizers/exif.rb b/lib/gitlab/sanitizers/exif.rb index 7e22bf4d7df..78c517c49d8 100644 --- a/lib/gitlab/sanitizers/exif.rb +++ b/lib/gitlab/sanitizers/exif.rb @@ -48,7 +48,7 @@ module Gitlab attr_reader :logger - def initialize(logger: Rails.logger) # rubocop:disable Gitlab/RailsLogger + def initialize(logger: Gitlab::AppLogger) @logger = logger end diff --git a/lib/gitlab/search/recent_issues.rb b/lib/gitlab/search/recent_issues.rb new file mode 100644 index 00000000000..413218da64d --- /dev/null +++ b/lib/gitlab/search/recent_issues.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module Search + class RecentIssues < RecentItems + private + + def type + Issue + end + + def finder + IssuesFinder + end + end + end +end diff --git a/lib/gitlab/search/recent_items.rb b/lib/gitlab/search/recent_items.rb new file mode 100644 index 00000000000..40d96ded275 --- /dev/null +++ b/lib/gitlab/search/recent_items.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Gitlab + module Search + # This is an abstract class used for storing/searching recently viewed + # items. The #type and #finder methods are the only ones needed to be + # implemented by classes inheriting from this. + class RecentItems + ITEMS_LIMIT = 100 + EXPIRES_AFTER = 7.days + + def initialize(user:, items_limit: ITEMS_LIMIT, expires_after: EXPIRES_AFTER) + @user = user + @items_limit = items_limit + @expires_after = expires_after + end + + def log_view(item) + with_redis do |redis| + redis.zadd(key, Time.now.to_f, item.id) + redis.expire(key, @expires_after) + + # There is a race condition here where we could end up removing an + # item from 2 places concurrently but this is fine since worst case + # scenario we remove an extra item from the end of the list. + if redis.zcard(key) > @items_limit + redis.zremrangebyrank(key, 0, 0) # Remove least recent + end + end + end + + def search(term) + ids = with_redis do |redis| + redis.zrevrange(key, 0, @items_limit - 1) + end.map(&:to_i) + + finder.new(@user, search: term, in: 'title').execute.reorder(nil).id_in_ordered(ids) # rubocop: disable CodeReuse/ActiveRecord + end + + private + + def with_redis(&blk) + Gitlab::Redis::SharedState.with(&blk) # rubocop: disable CodeReuse/ActiveRecord + end + + def key + "recent_items:#{type.name.downcase}:#{@user.id}" + end + + def type + raise NotImplementedError + end + + def finder + raise NotImplementedError + end + end + end +end diff --git a/lib/gitlab/search/recent_merge_requests.rb b/lib/gitlab/search/recent_merge_requests.rb new file mode 100644 index 00000000000..7b14e3b33e5 --- /dev/null +++ b/lib/gitlab/search/recent_merge_requests.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module Search + class RecentMergeRequests < RecentItems + private + + def type + MergeRequest + end + + def finder + MergeRequestsFinder + end + end + end +end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 3d5f64ce05b..06d8dca2f70 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -7,7 +7,7 @@ module Gitlab DEFAULT_PAGE = 1 DEFAULT_PER_PAGE = 20 - attr_reader :current_user, :query + attr_reader :current_user, :query, :filters # Limit search results by passed projects # It allows us to search only for projects user has access to @@ -19,11 +19,12 @@ module Gitlab # query attr_reader :default_project_filter - def initialize(current_user, limit_projects, query, default_project_filter: false) + def initialize(current_user, query, limit_projects = nil, default_project_filter: false, filters: {}) @current_user = current_user - @limit_projects = limit_projects || Project.all @query = query + @limit_projects = limit_projects || Project.all @default_project_filter = default_project_filter + @filters = filters end def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, without_count: true, preload_method: nil) @@ -186,10 +187,12 @@ module Gitlab params[:sort] = 'updated_desc' if query =~ /#(\d+)\z/ - params[:iids] = $1 + params[:iids] = Regexp.last_match(1) else params[:search] = query end + + params[:state] = filters[:state] if filters.key?(:state) end end diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb index 64a30fbe16c..4df6a50c8dd 100644 --- a/lib/gitlab/setup_helper.rb +++ b/lib/gitlab/setup_helper.rb @@ -28,6 +28,25 @@ module Gitlab end # rubocop:enable Rails/Output + module Workhorse + extend Gitlab::SetupHelper + class << self + def configuration_toml(dir, _) + config = { redis: { URL: redis_url } } + + TomlRB.dump(config) + end + + def redis_url + Gitlab::Redis::SharedState.url + end + + def get_config_path(dir) + File.join(dir, 'config.toml') + end + end + end + module Gitaly extend Gitlab::SetupHelper class << self diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 24f49f6b943..3419989c110 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -116,7 +116,7 @@ module Gitlab true rescue => e - Rails.logger.warn("Repository does not exist: #{e} at: #{disk_path}.git") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.warn("Repository does not exist: #{e} at: #{disk_path}.git") Gitlab::ErrorTracking.track_exception(e, path: disk_path, storage: storage) false diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb index cbd89b7629f..6f1d2ad23c1 100644 --- a/lib/gitlab/sherlock/query.rb +++ b/lib/gitlab/sherlock/query.rb @@ -105,7 +105,7 @@ module Gitlab query.each_line .map { |line| line.strip } .join("\n") - .gsub(PREFIX_NEWLINE) { "\n#{$1} " } + .gsub(PREFIX_NEWLINE) { "\n#{Regexp.last_match(1)} " } end end end diff --git a/lib/gitlab/sidekiq_daemon/memory_killer.rb b/lib/gitlab/sidekiq_daemon/memory_killer.rb index b8a4eedd620..e1a87a77f04 100644 --- a/lib/gitlab/sidekiq_daemon/memory_killer.rb +++ b/lib/gitlab/sidekiq_daemon/memory_killer.rb @@ -230,8 +230,10 @@ module Gitlab end def rss_increase_by_jobs - Gitlab::SidekiqDaemon::Monitor.instance.jobs.sum do |job| # rubocop:disable CodeReuse/ActiveRecord - rss_increase_by_job(job) + Gitlab::SidekiqDaemon::Monitor.instance.jobs_mutex.synchronize do + Gitlab::SidekiqDaemon::Monitor.instance.jobs.sum do |job| # rubocop:disable CodeReuse/ActiveRecord + rss_increase_by_job(job) + end end end diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb index bb0c18735bb..6417ec20960 100644 --- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb +++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb @@ -5,7 +5,7 @@ module Gitlab module DuplicateJobs class Client def call(worker_class, job, queue, _redis_pool, &block) - DuplicateJob.new(job, queue).schedule(&block) + ::Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(job, queue).schedule(&block) end end end diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb index 0dc53c61e84..5efd1b34d32 100644 --- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb +++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb @@ -71,7 +71,7 @@ module Gitlab end def droppable? - idempotent? && ::Feature.disabled?("disable_#{queue_name}_deduplication") + idempotent? && ::Feature.disabled?("disable_#{queue_name}_deduplication", type: :ops) end def scheduled_at diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb index a08310a58ff..6fdef4c354e 100644 --- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb +++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb @@ -7,7 +7,8 @@ module Gitlab UnknownStrategyError = Class.new(StandardError) STRATEGIES = { - until_executing: UntilExecuting + until_executing: UntilExecuting, + none: None }.freeze def self.for(name) diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/none.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/none.rb new file mode 100644 index 00000000000..cd101cd16b6 --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/none.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqMiddleware + module DuplicateJobs + module Strategies + # This strategy will never deduplicate a job + class None + def initialize(_duplicate_job) + end + + def schedule(_job) + yield + end + + def perform(_job) + yield + end + end + end + end + end +end diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb index 1d253ca90f3..41ec19f0da8 100644 --- a/lib/gitlab/snippet_search_results.rb +++ b/lib/gitlab/snippet_search_results.rb @@ -4,11 +4,8 @@ module Gitlab class SnippetSearchResults < SearchResults include SnippetsHelper - attr_reader :current_user - def initialize(current_user, query) - @current_user = current_user - @query = query + super(current_user, query) end def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil) diff --git a/lib/gitlab/sourcegraph.rb b/lib/gitlab/sourcegraph.rb index ec404ebd309..231d5aea129 100644 --- a/lib/gitlab/sourcegraph.rb +++ b/lib/gitlab/sourcegraph.rb @@ -12,8 +12,8 @@ module Gitlab !feature.off? end - def feature_enabled?(thing = nil) - feature.enabled?(thing) + def feature_enabled?(actor = nil) + feature.enabled?(actor) end private diff --git a/lib/gitlab/sql/except.rb b/lib/gitlab/sql/except.rb new file mode 100644 index 00000000000..82cbfa8d4ab --- /dev/null +++ b/lib/gitlab/sql/except.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module SQL + # Class for building SQL EXCEPT statements. + # + # ORDER BYs are dropped from the relations as the final sort order is not + # guaranteed any way. + # + # Example usage: + # + # except = Gitlab::SQL::Except.new([user.projects, user.personal_projects]) + # sql = except.to_sql + # + # Project.where("id IN (#{sql})") + class Except < SetOperator + def self.operator_keyword + 'EXCEPT' + end + end + end +end diff --git a/lib/gitlab/sql/intersect.rb b/lib/gitlab/sql/intersect.rb new file mode 100644 index 00000000000..c661db3d4c5 --- /dev/null +++ b/lib/gitlab/sql/intersect.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module SQL + # Class for building SQL INTERSECT statements. + # + # ORDER BYs are dropped from the relations as the final sort order is not + # guaranteed any way. + # + # Example usage: + # + # hierarchies = [group1.self_and_hierarchy, group2.self_and_hierarchy] + # intersect = Gitlab::SQL::Intersect.new(hierarchies) + # sql = intersect.to_sql + # + # Project.where("id IN (#{sql})") + class Intersect < SetOperator + def self.operator_keyword + 'INTERSECT' + end + end + end +end diff --git a/lib/gitlab/sql/set_operator.rb b/lib/gitlab/sql/set_operator.rb new file mode 100644 index 00000000000..d58a1415493 --- /dev/null +++ b/lib/gitlab/sql/set_operator.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Gitlab + module SQL + # Class for building SQL set operator statements (UNION, INTERSECT, and + # EXCEPT). + # + # ORDER BYs are dropped from the relations as the final sort order is not + # guaranteed any way. + # + # Example usage: + # + # union = Gitlab::SQL::Union.new([user.personal_projects, user.projects]) + # sql = union.to_sql + # + # Project.where("id IN (#{sql})") + class SetOperator + def initialize(relations, remove_duplicates: true) + @relations = relations + @remove_duplicates = remove_duplicates + end + + def self.operator_keyword + raise NotImplementedError + end + + def to_sql + # Some relations may include placeholders for prepared statements, these + # aren't incremented properly when joining relations together this way. + # By using "unprepared_statements" we remove the usage of placeholders + # (thus fixing this problem), at a slight performance cost. + fragments = ActiveRecord::Base.connection.unprepared_statement do + relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?) + end + + if fragments.any? + "(" + fragments.join(")\n#{operator_keyword_fragment}\n(") + ")" + else + 'NULL' + end + end + + # UNION [ALL] | INTERSECT [ALL] | EXCEPT [ALL] + def operator_keyword_fragment + remove_duplicates ? self.class.operator_keyword : "#{self.class.operator_keyword} ALL" + end + + private + + attr_reader :relations, :remove_duplicates + end + end +end diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb index b15f2ca385a..7fb3487a5e5 100644 --- a/lib/gitlab/sql/union.rb +++ b/lib/gitlab/sql/union.rb @@ -13,30 +13,9 @@ module Gitlab # sql = union.to_sql # # Project.where("id IN (#{sql})") - class Union - def initialize(relations, remove_duplicates: true) - @relations = relations - @remove_duplicates = remove_duplicates - end - - def to_sql - # Some relations may include placeholders for prepared statements, these - # aren't incremented properly when joining relations together this way. - # By using "unprepared_statements" we remove the usage of placeholders - # (thus fixing this problem), at a slight performance cost. - fragments = ActiveRecord::Base.connection.unprepared_statement do - @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?) - end - - if fragments.any? - "(" + fragments.join(")\n#{union_keyword}\n(") + ")" - else - 'NULL' - end - end - - def union_keyword - @remove_duplicates ? 'UNION' : 'UNION ALL' + class Union < SetOperator + def self.operator_keyword + 'UNION' end end end diff --git a/lib/gitlab/static_site_editor/config.rb b/lib/gitlab/static_site_editor/config.rb deleted file mode 100644 index d335a434335..00000000000 --- a/lib/gitlab/static_site_editor/config.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module StaticSiteEditor - class Config - SUPPORTED_EXTENSIONS = %w[.md].freeze - - def initialize(repository, ref, file_path, return_url) - @repository = repository - @ref = ref - @file_path = file_path - @return_url = return_url - @commit_id = repository.commit(ref)&.id if ref - end - - def payload - { - branch: ref, - path: file_path, - commit_id: commit_id, - project_id: project.id, - project: project.path, - namespace: project.namespace.full_path, - return_url: sanitize_url(return_url), - is_supported_content: supported_content?.to_s, - base_url: Gitlab::Routing.url_helpers.project_show_sse_path(project, full_path) - } - end - - private - - attr_reader :repository, :ref, :file_path, :return_url, :commit_id - - delegate :project, to: :repository - - def supported_content? - master_branch? && extension_supported? && file_exists? - end - - def master_branch? - ref == 'master' - end - - def extension_supported? - return true if file_path.end_with?('.md.erb') && Feature.enabled?(:sse_erb_support, project) - - SUPPORTED_EXTENSIONS.any? { |ext| file_path.end_with?(ext) } - end - - def file_exists? - commit_id.present? && !repository.blob_at(commit_id, file_path).nil? - end - - def full_path - "#{ref}/#{file_path}" - end - - def sanitize_url(url) - url if Gitlab::UrlSanitizer.valid_web?(url) - end - end - end -end diff --git a/lib/gitlab/static_site_editor/config/file_config.rb b/lib/gitlab/static_site_editor/config/file_config.rb new file mode 100644 index 00000000000..f647c85e1c8 --- /dev/null +++ b/lib/gitlab/static_site_editor/config/file_config.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module StaticSiteEditor + module Config + class FileConfig + def data + { + static_site_generator: 'middleman' + } + end + end + end + end +end diff --git a/lib/gitlab/static_site_editor/config/generated_config.rb b/lib/gitlab/static_site_editor/config/generated_config.rb new file mode 100644 index 00000000000..f3dce74a32f --- /dev/null +++ b/lib/gitlab/static_site_editor/config/generated_config.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Gitlab + module StaticSiteEditor + module Config + class GeneratedConfig + SUPPORTED_EXTENSIONS = %w[.md].freeze + + def initialize(repository, ref, path, return_url) + @repository = repository + @ref = ref + @path = path + @return_url = return_url + end + + def data + merge_requests_illustration_path = ActionController::Base.helpers.image_path('illustrations/merge_requests.svg') + { + branch: ref, + path: path, + commit_id: commit_id, + project_id: project.id, + project: project.path, + namespace: project.namespace.full_path, + return_url: sanitize_url(return_url), + is_supported_content: supported_content?.to_s, + base_url: Gitlab::Routing.url_helpers.project_show_sse_path(project, full_path), + merge_requests_illustration_path: merge_requests_illustration_path + } + end + + private + + attr_reader :repository, :ref, :path, :return_url + + delegate :project, to: :repository + + def commit_id + repository.commit(ref)&.id if ref + end + + def supported_content? + master_branch? && extension_supported? && file_exists? + end + + def master_branch? + ref == 'master' + end + + def extension_supported? + return true if path.end_with?('.md.erb') && Feature.enabled?(:sse_erb_support, project) + + SUPPORTED_EXTENSIONS.any? { |ext| path.end_with?(ext) } + end + + def file_exists? + commit_id.present? && !repository.blob_at(commit_id, path).nil? + end + + def full_path + "#{ref}/#{path}" + end + + def sanitize_url(url) + url if Gitlab::UrlSanitizer.valid_web?(url) + end + end + end + end +end diff --git a/lib/gitlab/submodule_links.rb b/lib/gitlab/submodule_links.rb index b0ee0877f30..38b10c5892d 100644 --- a/lib/gitlab/submodule_links.rb +++ b/lib/gitlab/submodule_links.rb @@ -4,14 +4,18 @@ module Gitlab class SubmoduleLinks include Gitlab::Utils::StrongMemoize + Urls = Struct.new(:web, :tree, :compare) + def initialize(repository) @repository = repository @cache_store = {} end - def for(submodule, sha) + def for(submodule, sha, diff_file = nil) submodule_url = submodule_url_for(sha, submodule.path) - SubmoduleHelper.submodule_links_for_url(submodule.id, submodule_url, repository) + old_submodule_id = old_submodule_id(submodule_url, diff_file) + urls = SubmoduleHelper.submodule_links_for_url(submodule.id, submodule_url, repository, old_submodule_id) + Urls.new(*urls) if urls.any? end private @@ -29,5 +33,15 @@ module Gitlab urls = submodule_urls_for(sha) urls && urls[path] end + + def old_submodule_id(submodule_url, diff_file) + return unless diff_file&.old_blob && diff_file&.old_content_sha + + # if the submodule url has changed from old_sha to sha, a compare link does not make sense + # + old_submodule_url = submodule_url_for(diff_file.old_content_sha, diff_file.old_blob.path) + + diff_file.old_blob.id if old_submodule_url == submodule_url + end end end diff --git a/lib/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb index 73187d8dea8..c702c6f1add 100644 --- a/lib/gitlab/task_helpers.rb +++ b/lib/gitlab/task_helpers.rb @@ -24,6 +24,8 @@ module Gitlab # Returns "yes" the user chose to continue # Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue def ask_to_continue + return if Gitlab::Utils.to_boolean(ENV['GITLAB_ASSUME_YES']) + answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no}) raise Gitlab::TaskAbortedByUserError unless answer == "yes" end diff --git a/lib/gitlab/template/finders/global_template_finder.rb b/lib/gitlab/template/finders/global_template_finder.rb index 3669d652fd3..9b39d386674 100644 --- a/lib/gitlab/template/finders/global_template_finder.rb +++ b/lib/gitlab/template/finders/global_template_finder.rb @@ -5,10 +5,10 @@ module Gitlab module Template module Finders class GlobalTemplateFinder < BaseTemplateFinder - def initialize(base_dir, extension, categories = {}, exclusions: []) + def initialize(base_dir, extension, categories = {}, excluded_patterns: []) @categories = categories @extension = extension - @exclusions = exclusions + @excluded_patterns = excluded_patterns super(base_dir) end @@ -43,7 +43,7 @@ module Gitlab private def excluded?(file_name) - @exclusions.include?(file_name) + @excluded_patterns.any? { |pattern| pattern.match?(file_name) } end def select_directory(file_name) diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb index 26a9dc9fd38..bb1e9db55fa 100644 --- a/lib/gitlab/template/gitlab_ci_yml_template.rb +++ b/lib/gitlab/template/gitlab_ci_yml_template.rb @@ -3,12 +3,16 @@ module Gitlab module Template class GitlabCiYmlTemplate < BaseTemplate + BASE_EXCLUDED_PATTERNS = [%r{\.latest$}].freeze + def content explanation = "# This file is a template, and might need editing before it works on your project." [explanation, super].join("\n") end class << self + include Gitlab::Utils::StrongMemoize + def extension '.gitlab-ci.yml' end @@ -22,10 +26,14 @@ module Gitlab } end - def disabled_templates - %w[ - Verify/Browser-Performance - ] + def excluded_patterns + strong_memoize(:excluded_patterns) do + BASE_EXCLUDED_PATTERNS + additional_excluded_patterns + end + end + + def additional_excluded_patterns + [%r{Verify/Browser-Performance}] end def base_dir @@ -34,7 +42,7 @@ module Gitlab def finder(project = nil) Gitlab::Template::Finders::GlobalTemplateFinder.new( - self.base_dir, self.extension, self.categories, exclusions: self.disabled_templates + self.base_dir, self.extension, self.categories, excluded_patterns: self.excluded_patterns ) end end diff --git a/lib/gitlab/testing/robots_blocker_middleware.rb b/lib/gitlab/testing/robots_blocker_middleware.rb new file mode 100644 index 00000000000..034492122df --- /dev/null +++ b/lib/gitlab/testing/robots_blocker_middleware.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# rubocop:disable Style/ClassVars +module Gitlab + module Testing + class RobotsBlockerMiddleware + @@block_requests = Concurrent::AtomicBoolean.new(false) + + # Block requests according to robots.txt. + # Any new requests disallowed by robots.txt will return an HTTP 503 status. + def self.block_requests! + @@block_requests.value = true + end + + # Allows the server to accept requests again. + def self.allow_requests! + @@block_requests.value = false + end + + def initialize(app) + @app = app + end + + def call(env) + request = Rack::Request.new(env) + + if block_requests? && Gitlab::RobotsTxt.disallowed?(request.path_info) + block_request(env) + else + @app.call(env) + end + end + + private + + def block_requests? + @@block_requests.true? + end + + def block_request(env) + [503, {}, []] + end + end + end +end diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb index 37688d6e0e7..02d354ec43a 100644 --- a/lib/gitlab/tracking.rb +++ b/lib/gitlab/tracking.rb @@ -47,8 +47,7 @@ module Gitlab cookie_domain: Gitlab::CurrentSettings.snowplow_cookie_domain, app_id: Gitlab::CurrentSettings.snowplow_app_id, form_tracking: additional_features, - link_click_tracking: additional_features, - iglu_registry_url: Gitlab::CurrentSettings.snowplow_iglu_registry_url + link_click_tracking: additional_features }.transform_keys! { |key| key.to_s.camelize(:lower).to_sym } end @@ -56,12 +55,19 @@ module Gitlab def snowplow @snowplow ||= SnowplowTracker::Tracker.new( - SnowplowTracker::AsyncEmitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname, protocol: 'https'), + emitter, SnowplowTracker::Subject.new, SNOWPLOW_NAMESPACE, Gitlab::CurrentSettings.snowplow_app_id ) end + + def emitter + SnowplowTracker::AsyncEmitter.new( + Gitlab::CurrentSettings.snowplow_collector_hostname, + protocol: 'https' + ) + end end end end diff --git a/lib/gitlab/tracking/incident_management.rb b/lib/gitlab/tracking/incident_management.rb index 5fa819b3696..df2a0658b36 100644 --- a/lib/gitlab/tracking/incident_management.rb +++ b/lib/gitlab/tracking/incident_management.rb @@ -35,6 +35,9 @@ module Gitlab }, pagerduty_active: { name: 'pagerduty_webhook' + }, + auto_close_incident: { + name: 'auto_close_incident' } }.with_indifferent_access.freeze end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 70efe86143e..89605ce5d07 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -38,6 +38,8 @@ module Gitlab .merge(usage_activity_by_stage(:usage_activity_by_stage_monthly, last_28_days_time_period)) .merge(analytics_unique_visits_data) .merge(compliance_unique_visits_data) + .merge(search_unique_visits_data) + .merge(redis_hll_counters) end end @@ -110,6 +112,8 @@ module Gitlab clusters_applications_jupyter: count(::Clusters::Applications::Jupyter.available), clusters_applications_cilium: count(::Clusters::Applications::Cilium.available), clusters_management_project: count(::Clusters::Cluster.with_management_project), + kubernetes_agents: count(::Clusters::Agent), + kubernetes_agents_with_token: distinct_count(::Clusters::AgentToken, :agent_id), in_review_folder: count(::Environment.in_review_folder), grafana_integrated_projects: count(GrafanaIntegration.enabled), groups: count(Group), @@ -129,6 +133,8 @@ module Gitlab lfs_objects: count(LfsObject), milestone_lists: count(List.milestone), milestones: count(Milestone), + projects_with_packages: distinct_count(::Packages::Package, :project_id), + packages: count(::Packages::Package), pages_domains: count(PagesDomain), pool_repositories: count(PoolRepository), projects: count(Project), @@ -160,7 +166,8 @@ module Gitlab user_preferences_usage, ingress_modsecurity_usage, container_expiration_policies_usage, - service_desk_counts + service_desk_counts, + snowplow_event_counts ).tap do |data| data[:snippets] = data[:personal_snippets] + data[:project_snippets] end @@ -168,6 +175,19 @@ module Gitlab end # rubocop: enable Metrics/AbcSize + def snowplow_event_counts(time_period: {}) + return {} unless report_snowplow_events? + + { + promoted_issues: count( + self_monitoring_project + .product_analytics_events + .by_category_and_action('epics', 'promote') + .where(time_period) + ) + } + end + def system_usage_data_monthly { counts_monthly: { @@ -176,9 +196,12 @@ module Gitlab successful_deployments: deployment_count(Deployment.success.where(last_28_days_time_period)), failed_deployments: deployment_count(Deployment.failed.where(last_28_days_time_period)), # rubocop: enable UsageData/LargeTable: + packages: count(::Packages::Package.where(last_28_days_time_period)), personal_snippets: count(PersonalSnippet.where(last_28_days_time_period)), project_snippets: count(ProjectSnippet.where(last_28_days_time_period)) - }.tap do |data| + }.merge( + snowplow_event_counts(time_period: last_28_days_time_period(column: :collector_tstamp)) + ).tap do |data| data[:snippets] = data[:personal_snippets] + data[:project_snippets] end } @@ -240,7 +263,8 @@ module Gitlab Gitlab::UsageDataCounters::ProductivityAnalyticsCounter, Gitlab::UsageDataCounters::SourceCodeCounter, Gitlab::UsageDataCounters::MergeRequestCounter, - Gitlab::UsageDataCounters::DesignsCounter + Gitlab::UsageDataCounters::DesignsCounter, + Gitlab::UsageDataCounters::KubernetesAgentCounter ] end @@ -264,6 +288,9 @@ module Gitlab database: { adapter: alt_usage_data { Gitlab::Database.adapter_name }, version: alt_usage_data { Gitlab::Database.version } + }, + mail: { + smtp_server: alt_usage_data { ActionMailer::Base.smtp_settings[:address] } } } end @@ -371,7 +398,9 @@ module Gitlab # so we can just check for subdomains of atlassian.net results = { projects_jira_server_active: 0, - projects_jira_cloud_active: 0 + projects_jira_cloud_active: 0, + projects_jira_dvcs_cloud_active: count(ProjectFeatureUsage.with_jira_dvcs_integration_enabled), + projects_jira_dvcs_server_active: count(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) } # rubocop: disable UsageData/LargeTable: @@ -399,7 +428,7 @@ module Gitlab { jira_imports_total_imported_count: count(finished_jira_imports), jira_imports_projects_count: distinct_count(finished_jira_imports, :project_id), - jira_imports_total_imported_issues_count: alt_usage_data { JiraImportState.finished_imports_count } + jira_imports_total_imported_issues_count: sum(JiraImportState.finished, :imported_issues_count) } # rubocop: enable UsageData/LargeTable end @@ -409,7 +438,7 @@ module Gitlab def successful_deployments_with_cluster(scope) scope .joins(cluster: :deployments) - .merge(Clusters::Cluster.enabled) + .merge(::Clusters::Cluster.enabled) .merge(Deployment.success) end # rubocop: enable UsageData/LargeTable @@ -419,16 +448,17 @@ module Gitlab {} # augmented in EE end - # rubocop: disable CodeReuse/ActiveRecord def merge_requests_users(time_period) - distinct_count( - Event.where(target_type: Event::TARGET_TYPES[:merge_request].to_s).where(time_period), - :author_id, - start: user_minimum_id, - finish: user_maximum_id - ) + counter = Gitlab::UsageDataCounters::TrackUniqueEvents + + redis_usage_data do + counter.count_unique_events( + event_action: Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION, + date_from: time_period[:created_at].first, + date_to: time_period[:created_at].last + ) + end end - # rubocop: enable CodeReuse/ActiveRecord def installation_type if Rails.env.production? @@ -438,8 +468,8 @@ module Gitlab end end - def last_28_days_time_period - { created_at: 28.days.ago..Time.current } + def last_28_days_time_period(column: :created_at) + { column => 28.days.ago..Time.current } end # Source: https://gitlab.com/gitlab-data/analytics/blob/master/transform/snowflake-dbt/data/ping_metrics_to_stage_mapping_data.csv @@ -510,7 +540,22 @@ module Gitlab events: distinct_count(::Event.where(time_period), :author_id), groups: distinct_count(::GroupMember.where(time_period), :user_id), users_created: count(::User.where(time_period), start: user_minimum_id, finish: user_maximum_id), - omniauth_providers: filtered_omniauth_provider_names.reject { |name| name == 'group_saml' } + omniauth_providers: filtered_omniauth_provider_names.reject { |name| name == 'group_saml' }, + projects_imported: { + gitlab_project: projects_imported_count('gitlab_project', time_period), + gitlab: projects_imported_count('gitlab', time_period), + github: projects_imported_count('github', time_period), + bitbucket: projects_imported_count('bitbucket', time_period), + bitbucket_server: projects_imported_count('bitbucket_server', time_period), + gitea: projects_imported_count('gitea', time_period), + git: projects_imported_count('git', time_period), + manifest: projects_imported_count('manifest', time_period) + }, + issues_imported: { + jira: distinct_count(::JiraImportState.where(time_period), :user_id), + fogbugz: projects_imported_count('fogbugz', time_period), + phabricator: projects_imported_count('phabricator', time_period) + } } end # rubocop: enable CodeReuse/ActiveRecord @@ -527,9 +572,13 @@ module Gitlab end # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def usage_activity_by_stage_package(time_period) - {} + { + projects_with_packages: distinct_count(::Project.with_packages.where(time_period), :creator_id) + } end + # rubocop: enable CodeReuse/ActiveRecord # Omitted because no user, creator or author associated: `boards`, `labels`, `milestones`, `uploads` # Omitted because too expensive: `epics_deepest_relationship_level` @@ -542,7 +591,10 @@ module Gitlab projects: distinct_count(::Project.where(time_period), :creator_id), todos: distinct_count(::Todo.where(time_period), :author_id), service_desk_enabled_projects: distinct_count_service_desk_enabled_projects(time_period), - service_desk_issues: count(::Issue.service_desk.where(time_period)) + service_desk_issues: count(::Issue.service_desk.where(time_period)), + projects_jira_active: distinct_count(::Project.with_active_jira_services.where(time_period), :creator_id), + projects_jira_dvcs_cloud_active: distinct_count(::Project.with_active_jira_services.with_jira_dvcs_cloud.where(time_period), :creator_id), + projects_jira_dvcs_server_active: distinct_count(::Project.with_active_jira_services.with_jira_dvcs_server.where(time_period), :creator_id) } end # rubocop: enable CodeReuse/ActiveRecord @@ -583,9 +635,13 @@ module Gitlab {} end + def redis_hll_counters + { redis_hll_counters: ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events_data } + end + def analytics_unique_visits_data - results = ::Gitlab::Analytics::UniqueVisits.analytics_ids.each_with_object({}) do |target_id, hash| - hash[target_id] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target_id) } + results = ::Gitlab::Analytics::UniqueVisits.analytics_events.each_with_object({}) do |target, hash| + hash[target] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target) } end results['analytics_unique_visits_for_any_target'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics) } results['analytics_unique_visits_for_any_target_monthly'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics, start_date: 4.weeks.ago.to_date, end_date: Date.current) } @@ -594,8 +650,8 @@ module Gitlab end def compliance_unique_visits_data - results = ::Gitlab::Analytics::UniqueVisits.compliance_ids.each_with_object({}) do |target_id, hash| - hash[target_id] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target_id) } + results = ::Gitlab::Analytics::UniqueVisits.compliance_events.each_with_object({}) do |target, hash| + hash[target] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target) } end results['compliance_unique_visits_for_any_target'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :compliance) } results['compliance_unique_visits_for_any_target_monthly'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :compliance, start_date: 4.weeks.ago.to_date, end_date: Date.current) } @@ -603,41 +659,53 @@ module Gitlab { compliance_unique_visits: results } end + def search_unique_visits_data + events = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category('search') + results = events.each_with_object({}) do |event, hash| + hash[event] = redis_usage_data { ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: event, start_date: 7.days.ago.to_date, end_date: Date.current) } + end + + results['search_unique_visits_for_any_target_weekly'] = redis_usage_data { ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, start_date: 7.days.ago.to_date, end_date: Date.current) } + results['search_unique_visits_for_any_target_monthly'] = redis_usage_data { ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, start_date: 4.weeks.ago.to_date, end_date: Date.current) } + + { search_unique_visits: results } + end + def action_monthly_active_users(time_period) - counter = Gitlab::UsageDataCounters::TrackUniqueActions + date_range = { date_from: time_period[:created_at].first, date_to: time_period[:created_at].last } - project_count = redis_usage_data do - counter.count_unique( - event_action: Gitlab::UsageDataCounters::TrackUniqueActions::PUSH_ACTION, - date_from: time_period[:created_at].first, - date_to: time_period[:created_at].last - ) - end + event_monthly_active_users(date_range) + .merge!(ide_monthly_active_users(date_range)) + end - design_count = redis_usage_data do - counter.count_unique( - event_action: Gitlab::UsageDataCounters::TrackUniqueActions::DESIGN_ACTION, - date_from: time_period[:created_at].first, - date_to: time_period[:created_at].last - ) - end + private - wiki_count = redis_usage_data do - counter.count_unique( - event_action: Gitlab::UsageDataCounters::TrackUniqueActions::WIKI_ACTION, - date_from: time_period[:created_at].first, - date_to: time_period[:created_at].last - ) + def event_monthly_active_users(date_range) + data = { + action_monthly_active_users_project_repo: Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION, + action_monthly_active_users_design_management: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION, + action_monthly_active_users_wiki_repo: Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION + } + + data.each do |key, event| + data[key] = redis_usage_data { Gitlab::UsageDataCounters::TrackUniqueEvents.count_unique_events(event_action: event, **date_range) } end + end + + def ide_monthly_active_users(date_range) + counter = Gitlab::UsageDataCounters::EditorUniqueCounter { - action_monthly_active_users_project_repo: project_count, - action_monthly_active_users_design_management: design_count, - action_monthly_active_users_wiki_repo: wiki_count + 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_ide_edit: redis_usage_data { counter.count_edit_using_editor(date_range) } } end - private + def report_snowplow_events? + self_monitoring_project && Feature.enabled?(:product_analytics, self_monitoring_project) + end def distinct_count_service_desk_enabled_projects(time_period) project_creator_id_start = user_minimum_id @@ -716,6 +784,22 @@ module Gitlab end end + def project_minimum_id + strong_memoize(:project_minimum_id) do + ::Project.minimum(:id) + end + end + + def project_maximum_id + strong_memoize(:project_maximum_id) do + ::Project.maximum(:id) + end + end + + def self_monitoring_project + Gitlab::CurrentSettings.self_monitoring_project + end + def clear_memoized clear_memoization(:issue_minimum_id) clear_memoization(:issue_maximum_id) @@ -726,14 +810,14 @@ module Gitlab clear_memoization(:deployment_maximum_id) clear_memoization(:approval_merge_request_rule_minimum_id) clear_memoization(:approval_merge_request_rule_maximum_id) + clear_memoization(:project_minimum_id) + clear_memoization(:project_maximum_id) end # rubocop: disable CodeReuse/ActiveRecord - # rubocop: disable UsageData/DistinctCountByLargeForeignKey def cluster_applications_user_distinct_count(applications, time_period) distinct_count(applications.where(time_period).available.joins(:cluster), 'clusters.user_id') end - # rubocop: enable UsageData/DistinctCountByLargeForeignKey def clusters_user_distinct_count(clusters, time_period) distinct_count(clusters.where(time_period), :user_id) @@ -755,6 +839,10 @@ module Gitlab def deployment_count(relation) count relation, start: deployment_minimum_id, finish: deployment_maximum_id end + + def projects_imported_count(from, time_period) + distinct_count(::Project.imported_from(from).where(time_period), :creator_id) # rubocop: disable CodeReuse/ActiveRecord + end end end end diff --git a/lib/gitlab/usage_data/topology.rb b/lib/gitlab/usage_data/topology.rb index edc4dc75750..7f7854c3eb1 100644 --- a/lib/gitlab/usage_data/topology.rb +++ b/lib/gitlab/usage_data/topology.rb @@ -40,9 +40,10 @@ module Gitlab private def topology_fetch_all_data - with_prometheus_client(fallback: {}) do |client| + with_prometheus_client(fallback: {}, verify: false) do |client| { application_requests_per_hour: topology_app_requests_per_hour(client), + query_apdex_weekly_average: topology_query_apdex_weekly_average(client), nodes: topology_node_data(client) }.compact end @@ -63,6 +64,16 @@ module Gitlab (result['value'].last.to_f * 1.hour).to_i end + def topology_query_apdex_weekly_average(client) + result = query_safely('gitlab_usage_ping:sql_duration_apdex:ratio_rate5m', 'query_apdex', fallback: nil) do |query| + client.query(aggregate_one_week(query)).first + end + + return unless result + + result['value'].last.to_f + end + def topology_node_data(client) # node-level data by_instance_mem = topology_node_memory(client) diff --git a/lib/gitlab/usage_data_counters/editor_unique_counter.rb b/lib/gitlab/usage_data_counters/editor_unique_counter.rb new file mode 100644 index 00000000000..b68d50ee419 --- /dev/null +++ b/lib/gitlab/usage_data_counters/editor_unique_counter.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Gitlab + module UsageDataCounters + module EditorUniqueCounter + 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_CATEGORY = 'ide_edit' + + class << self + def track_web_ide_edit_action(author:, time: Time.zone.now) + track_unique_action(EDIT_BY_WEB_IDE, author, time) + end + + def count_web_ide_edit_actions(date_from:, date_to:) + count_unique(EDIT_BY_WEB_IDE, date_from, date_to) + end + + def track_sfe_edit_action(author:, time: Time.zone.now) + track_unique_action(EDIT_BY_SFE, author, time) + end + + def count_sfe_edit_actions(date_from:, date_to:) + count_unique(EDIT_BY_SFE, date_from, date_to) + end + + def track_snippet_editor_edit_action(author:, time: Time.zone.now) + track_unique_action(EDIT_BY_SNIPPET_EDITOR, author, time) + end + + def count_snippet_editor_edit_actions(date_from:, date_to:) + count_unique(EDIT_BY_SNIPPET_EDITOR, date_from, date_to) + end + + def count_edit_using_editor(date_from:, date_to:) + events = Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(EDIT_CATEGORY) + count_unique(events, date_from, date_to) + end + + private + + def track_unique_action(action, author, time) + return unless Feature.enabled?(:track_editor_edit_actions, default_enabled: true) + return unless author + + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(author.id, action, time) + end + + def count_unique(actions, date_from, date_to) + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: actions, start_date: date_from, end_date: date_to) + end + end + end + end +end diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb index c9c39225068..53bf6daea4c 100644 --- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb +++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb @@ -31,7 +31,11 @@ module Gitlab # * Track event: Gitlab::UsageDataCounters::HLLRedisCounter.track_event(user_id, 'g_compliance_dashboard') # * Get unique counts per user: Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'g_compliance_dashboard', start_date: 28.days.ago, end_date: Date.current) class << self + include Gitlab::Utils::UsageData + def track_event(entity_id, event_name, time = Time.zone.now) + return unless Gitlab::CurrentSettings.usage_ping_enabled? + event = event_for(event_name) raise UnknownEvent.new("Unknown event #{event_name}") unless event.present? @@ -50,15 +54,51 @@ module Gitlab keys = keys_for_aggregation(aggregation, events: events, start_date: start_date, end_date: end_date) - Gitlab::Redis::HLL.count(keys: keys) + redis_usage_data { Gitlab::Redis::HLL.count(keys: keys) } + end + + def categories + @categories ||= known_events.map { |event| event[:category] }.uniq end + # @param category [String] the category name + # @return [Array<String>] list of event names for given category def events_for_category(category) - known_events.select { |event| event[:category] == category }.map { |event| event[:name] } + known_events.select { |event| event[:category] == category.to_s }.map { |event| event[:name] } + end + + def unique_events_data + categories.each_with_object({}) do |category, category_results| + events_names = events_for_category(category) + + event_results = events_names.each_with_object({}) do |event, hash| + hash[event] = unique_events(event_names: event, start_date: 7.days.ago.to_date, end_date: Date.current) + end + + if eligible_for_totals?(events_names) + event_results["#{category}_total_unique_counts_weekly"] = unique_events(event_names: events_names, start_date: 7.days.ago.to_date, end_date: Date.current) + event_results["#{category}_total_unique_counts_monthly"] = unique_events(event_names: events_names, start_date: 4.weeks.ago.to_date, end_date: Date.current) + end + + category_results["#{category}"] = event_results + end + end + + def known_event?(event_name) + event_for(event_name).present? end private + # Allow to add totals for events that are in the same redis slot, category and have the same aggregation level + # and if there are more than 1 event + def eligible_for_totals?(events_names) + return false if events_names.size <= 1 + + events = events_for(events_names) + events_in_same_slot?(events) && events_in_same_category?(events) && events_same_aggregation?(events) + end + def keys_for_aggregation(aggregation, events:, start_date:, end_date:) if aggregation.to_sym == :daily daily_redis_keys(events: events, start_date: start_date, end_date: end_date) @@ -76,8 +116,11 @@ module Gitlab end def events_in_same_slot?(events) + # if we check one event then redis_slot is only one to check + return true if events.size == 1 + slot = events.first[:redis_slot] - events.all? { |event| event[:redis_slot] == slot } + events.all? { |event| event[:redis_slot].present? && event[:redis_slot] == slot } end def events_in_same_category?(events) @@ -91,7 +134,7 @@ module Gitlab end def expiry(event) - return event[:expiry] if event[:expiry].present? + return event[:expiry].days if event[:expiry].present? event[:aggregation].to_sym == :daily ? DEFAULT_DAILY_KEY_EXPIRY_LENGTH : DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH end diff --git a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb new file mode 100644 index 00000000000..fc1b5a59487 --- /dev/null +++ b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Gitlab + module UsageDataCounters + module IssueActivityUniqueCounter + ISSUE_TITLE_CHANGED = 'g_project_management_issue_title_changed' + ISSUE_DESCRIPTION_CHANGED = 'g_project_management_issue_description_changed' + ISSUE_ASSIGNEE_CHANGED = 'g_project_management_issue_assignee_changed' + ISSUE_MADE_CONFIDENTIAL = 'g_project_management_issue_made_confidential' + ISSUE_MADE_VISIBLE = 'g_project_management_issue_made_visible' + ISSUE_CATEGORY = 'issues_edit' + + class << self + def track_issue_title_changed_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_TITLE_CHANGED, author, time) + end + + def track_issue_description_changed_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_DESCRIPTION_CHANGED, author, time) + end + + def track_issue_assignee_changed_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_ASSIGNEE_CHANGED, author, time) + end + + def track_issue_made_confidential_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_MADE_CONFIDENTIAL, author, time) + end + + def track_issue_made_visible_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_MADE_VISIBLE, author, time) + end + + private + + def track_unique_action(action, author, time) + return unless Feature.enabled?(:track_issue_activity_actions) + return unless author + + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(author.id, action, time) + end + end + end + end +end diff --git a/lib/gitlab/usage_data_counters/known_events.yml b/lib/gitlab/usage_data_counters/known_events.yml index b7e516fa8b1..25e7f858bb1 100644 --- a/lib/gitlab/usage_data_counters/known_events.yml +++ b/lib/gitlab/usage_data_counters/known_events.yml @@ -3,86 +3,206 @@ - name: g_compliance_dashboard redis_slot: compliance category: compliance - expiry: 84 # expiration time in days, equivalent to 12 weeks aggregation: weekly - name: g_compliance_audit_events category: compliance redis_slot: compliance - expiry: 84 aggregation: weekly - name: i_compliance_audit_events category: compliance redis_slot: compliance - expiry: 84 aggregation: weekly - name: i_compliance_credential_inventory category: compliance redis_slot: compliance - expiry: 84 + aggregation: weekly +- name: a_compliance_audit_events_api + category: compliance + redis_slot: compliance aggregation: weekly # Analytics category - name: g_analytics_contribution category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: g_analytics_insights category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: g_analytics_issues category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: g_analytics_productivity category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: g_analytics_valuestream category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: p_analytics_pipelines category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: p_analytics_code_reviews category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: p_analytics_valuestream category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: p_analytics_insights category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: p_analytics_issues category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: p_analytics_repo category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: i_analytics_cohorts category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly - name: i_analytics_dev_ops_score category: analytics redis_slot: analytics - expiry: 84 aggregation: weekly +- name: g_analytics_merge_request + category: analytics + redis_slot: analytics + aggregation: weekly +- name: p_analytics_merge_request + category: analytics + redis_slot: analytics + aggregation: weekly +- name: i_analytics_instance_statistics + category: analytics + redis_slot: analytics + aggregation: weekly +- name: g_edit_by_web_ide + category: ide_edit + redis_slot: edit + expiry: 29 + aggregation: daily +- name: g_edit_by_sfe + category: ide_edit + redis_slot: edit + expiry: 29 + aggregation: daily +- name: g_edit_by_snippet_ide + category: ide_edit + redis_slot: edit + expiry: 29 + aggregation: daily +- name: i_search_total + category: search + redis_slot: search + aggregation: weekly +- name: i_search_advanced + category: search + redis_slot: search + aggregation: weekly +- name: i_search_paid + category: search + redis_slot: search + aggregation: weekly +- name: wiki_action + category: source_code + aggregation: daily +- name: design_action + category: source_code + aggregation: daily +- name: project_action + category: source_code + aggregation: daily +- name: merge_request_action + category: source_code + aggregation: daily +- name: i_source_code_code_intelligence + redis_slot: source_code + category: source_code + aggregation: daily +# Incident management +- name: incident_management_alert_status_changed + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_alert_assigned + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_alert_todo + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_incident_created + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_incident_reopened + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_incident_closed + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_incident_assigned + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_incident_todo + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_incident_comment + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_incident_zoom_meeting + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_incident_published + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_incident_relate + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_incident_unrelate + redis_slot: incident_management + category: incident_management + aggregation: weekly +- name: incident_management_incident_change_confidential + redis_slot: incident_management + category: incident_management + aggregation: weekly +# Project Management group +- name: g_project_management_issue_title_changed + category: issues_edit + redis_slot: project_management + aggregation: daily +- name: g_project_management_issue_description_changed + category: issues_edit + redis_slot: project_management + aggregation: daily +- name: g_project_management_issue_assignee_changed + category: issues_edit + redis_slot: project_management + aggregation: daily +- name: g_project_management_issue_made_confidential + category: issues_edit + redis_slot: project_management + aggregation: daily +- name: g_project_management_issue_made_visible + category: issues_edit + redis_slot: project_management + aggregation: daily diff --git a/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb b/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb new file mode 100644 index 00000000000..eae42bdc4a1 --- /dev/null +++ b/lib/gitlab/usage_data_counters/kubernetes_agent_counter.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module UsageDataCounters + class KubernetesAgentCounter < BaseCounter + PREFIX = 'kubernetes_agent' + KNOWN_EVENTS = %w[gitops_sync].freeze + + class << self + def increment_gitops_sync(incr) + raise ArgumentError, 'must be greater than or equal to zero' if incr < 0 + + # rather then hitting redis for this no-op, we return early + # note: redis returns the increment, so we mimic this here + return 0 if incr == 0 + + increment_by(redis_key(:gitops_sync), incr) + end + end + end + end +end diff --git a/lib/gitlab/usage_data_counters/redis_counter.rb b/lib/gitlab/usage_data_counters/redis_counter.rb index 75d5a75e3a4..2406f771fd8 100644 --- a/lib/gitlab/usage_data_counters/redis_counter.rb +++ b/lib/gitlab/usage_data_counters/redis_counter.rb @@ -9,6 +9,12 @@ module Gitlab Gitlab::Redis::SharedState.with { |redis| redis.incr(redis_counter_key) } end + def increment_by(redis_counter_key, incr) + return unless Gitlab::CurrentSettings.usage_ping_enabled + + Gitlab::Redis::SharedState.with { |redis| redis.incrby(redis_counter_key, incr) } + end + def total_count(redis_counter_key) Gitlab::Redis::SharedState.with { |redis| redis.get(redis_counter_key).to_i } end diff --git a/lib/gitlab/usage_data_counters/track_unique_actions.rb b/lib/gitlab/usage_data_counters/track_unique_events.rb index 0df982572a4..7053744b665 100644 --- a/lib/gitlab/usage_data_counters/track_unique_actions.rb +++ b/lib/gitlab/usage_data_counters/track_unique_events.rb @@ -2,12 +2,11 @@ module Gitlab module UsageDataCounters - module TrackUniqueActions - KEY_EXPIRY_LENGTH = 29.days - + module TrackUniqueEvents WIKI_ACTION = :wiki_action DESIGN_ACTION = :design_action PUSH_ACTION = :project_action + MERGE_REQUEST_ACTION = :merge_request_action ACTION_TRANSFORMATIONS = HashWithIndifferentAccess.new({ wiki: { @@ -22,26 +21,30 @@ module Gitlab }, project: { pushed: PUSH_ACTION + }, + merge_request: { + closed: MERGE_REQUEST_ACTION, + merged: MERGE_REQUEST_ACTION, + created: MERGE_REQUEST_ACTION, + commented: MERGE_REQUEST_ACTION } }).freeze class << self def track_event(event_action:, event_target:, author_id:, time: Time.zone.now) - return unless Gitlab::CurrentSettings.usage_ping_enabled return unless valid_target?(event_target) return unless valid_action?(event_action) transformed_target = transform_target(event_target) transformed_action = transform_action(event_action, transformed_target) - target_key = key(transformed_action, time) - Gitlab::Redis::HLL.add(key: target_key, value: author_id, expiry: KEY_EXPIRY_LENGTH) - end + return unless Gitlab::UsageDataCounters::HLLRedisCounter.known_event?(transformed_action.to_s) - def count_unique(event_action:, date_from:, date_to:) - keys = (date_from.to_date..date_to.to_date).map { |date| key(event_action, date) } + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(author_id, transformed_action.to_s, time) + end - Gitlab::Redis::HLL.count(keys: keys) + def count_unique_events(event_action:, date_from:, date_to:) + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: event_action.to_s, start_date: date_from, end_date: date_to) end private @@ -61,11 +64,6 @@ module Gitlab def valid_action?(action) Event.actions.key?(action) end - - def key(event_action, date) - year_day = date.strftime('%G-%j') - "#{year_day}-{#{event_action}}" - end end end end diff --git a/lib/gitlab/usage_data_queries.rb b/lib/gitlab/usage_data_queries.rb new file mode 100644 index 00000000000..bacd63ab282 --- /dev/null +++ b/lib/gitlab/usage_data_queries.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + # This class is used by the `gitlab:usage_data:dump_sql` rake tasks to output SQL instead of running it. + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41091 + class UsageDataQueries < UsageData + class << self + def count(relation, column = nil, *rest) + raw_sql(relation, column) + end + + def distinct_count(relation, column = nil, *rest) + raw_sql(relation, column, :distinct) + end + + def redis_usage_data(counter = nil, &block) + if block_given? + { redis_usage_data_block: block.to_s } + elsif counter.present? + { redis_usage_data_counter: counter } + end + end + + def sum(relation, column, *rest) + relation.select(relation.all.table[column].sum).to_sql # rubocop:disable CodeReuse/ActiveRecord + end + + private + + def raw_sql(relation, column, distinct = nil) + column ||= relation.primary_key + relation.select(relation.all.table[column].count(distinct)).to_sql + end + end + end +end diff --git a/lib/gitlab/utils/gzip.rb b/lib/gitlab/utils/gzip.rb new file mode 100644 index 00000000000..898be651554 --- /dev/null +++ b/lib/gitlab/utils/gzip.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Utils + module Gzip + def gzip_compress(data) + # .compress returns ASCII-8BIT, so we need to force the encoding to + # UTF-8 before caching it in redis, else we risk encoding mismatch + # errors. + # + ActiveSupport::Gzip.compress(data).force_encoding("UTF-8") + rescue Zlib::GzipFile::Error + data + end + + def gzip_decompress(data) + # Since we could be dealing with an already populated cache full of data + # that isn't gzipped, we want to also check to see if the data is + # gzipped before we attempt to .decompress it, thus we check the first + # 2 bytes for "\x1F\x8B" to confirm it is a gzipped string. While a + # non-gzipped string will raise a Zlib::GzipFile::Error, which we're + # rescuing, we don't want to count on rescue for control flow. + # + data[0..1] == "\x1F\x8B" ? ActiveSupport::Gzip.decompress(data) : data + rescue Zlib::GzipFile::Error + data + end + end + end +end diff --git a/lib/gitlab/utils/markdown.rb b/lib/gitlab/utils/markdown.rb index 82c4a0e3b23..e783ac785cc 100644 --- a/lib/gitlab/utils/markdown.rb +++ b/lib/gitlab/utils/markdown.rb @@ -4,11 +4,13 @@ module Gitlab module Utils module Markdown PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze + PRODUCT_SUFFIX = /\s*\**\((core|starter|premium|ultimate)(\s+only)?\)\**/.freeze def string_to_anchor(string) string .strip .downcase + .gsub(PRODUCT_SUFFIX, '') .gsub(PUNCTUATION_REGEXP, '') # remove punctuation .tr(' ', '-') # replace spaces with dash .squeeze('-') # replace multiple dashes with one diff --git a/lib/gitlab/utils/usage_data.rb b/lib/gitlab/utils/usage_data.rb index 36046ca14bf..ca6a36c9cea 100644 --- a/lib/gitlab/utils/usage_data.rb +++ b/lib/gitlab/utils/usage_data.rb @@ -59,6 +59,12 @@ module Gitlab 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 + FALLBACK + end + def alt_usage_data(value = nil, fallback: FALLBACK, &block) if block_given? yield @@ -77,11 +83,11 @@ module Gitlab end end - def with_prometheus_client(fallback: nil) - return fallback unless Gitlab::Prometheus::Internal.prometheus_enabled? + def with_prometheus_client(fallback: nil, verify: true) + client = prometheus_client(verify: verify) + return fallback unless client - prometheus_address = Gitlab::Prometheus::Internal.uri - yield Gitlab::PrometheusClient.new(prometheus_address, allow_local_requests: true) + yield client end def measure_duration @@ -96,8 +102,41 @@ module Gitlab yield.merge(key => Time.current) end + # @param event_name [String] the event name + # @param values [Array|String] the values counted + def track_usage_event(event_name, values) + return unless Feature.enabled?(:"usage_data_#{event_name}", default_enabled: true) + return unless Gitlab::CurrentSettings.usage_ping_enabled? + + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(values, event_name.to_s) + end + private + def prometheus_client(verify:) + server_address = prometheus_server_address + + return unless server_address + + # There really is not a way to discover whether a Prometheus connection is using TLS or not + # Try TLS first because HTTPS will return fast if failed. + %w[https http].find do |scheme| + api_url = "#{scheme}://#{server_address}" + client = Gitlab::PrometheusClient.new(api_url, allow_local_requests: true, verify: verify) + break client if client.ready? + rescue + nil + end + end + + def prometheus_server_address + if Gitlab::Prometheus::Internal.prometheus_enabled? + Gitlab::Prometheus::Internal.server_address + elsif Gitlab::Consul::Internal.api_url + Gitlab::Consul::Internal.discover_prometheus_server_address + end + end + def redis_usage_counter yield rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent diff --git a/lib/gitlab/web_ide/config.rb b/lib/gitlab/web_ide/config.rb index 3b1fa162b53..b2ab5c0b6e3 100644 --- a/lib/gitlab/web_ide/config.rb +++ b/lib/gitlab/web_ide/config.rb @@ -34,6 +34,10 @@ module Gitlab @global.terminal_value end + def schemas_value + @global.schemas_value + end + private def build_config(config, opts = {}) diff --git a/lib/gitlab/web_ide/config/entry/global.rb b/lib/gitlab/web_ide/config/entry/global.rb index 50c3f2d294f..2c67c7d02d4 100644 --- a/lib/gitlab/web_ide/config/entry/global.rb +++ b/lib/gitlab/web_ide/config/entry/global.rb @@ -12,18 +12,22 @@ module Gitlab include ::Gitlab::Config::Entry::Configurable include ::Gitlab::Config::Entry::Attributable - ALLOWED_KEYS = %i[terminal].freeze + def self.allowed_keys + %i[terminal].freeze + end validations do - validates :config, allowed_keys: ALLOWED_KEYS + validates :config, allowed_keys: Global.allowed_keys end + attributes allowed_keys + entry :terminal, Entry::Terminal, description: 'Configuration of the webide terminal.' - - attributes :terminal end end end end end + +::Gitlab::WebIde::Config::Entry::Global.prepend_if_ee('EE::Gitlab::WebIde::Config::Entry::Global') diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index e3b1cb3d016..8a5acd242d9 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -156,10 +156,11 @@ module Gitlab ] end - def send_scaled_image(location, width) + def send_scaled_image(location, width, content_type) params = { 'Location' => location, - 'Width' => width + 'Width' => width, + 'ContentType' => content_type } [ diff --git a/lib/gitlab_danger.rb b/lib/gitlab_danger.rb index a98ac9200da..a906beda80e 100644 --- a/lib/gitlab_danger.rb +++ b/lib/gitlab_danger.rb @@ -13,6 +13,7 @@ class GitlabDanger commit_messages telemetry utility_css + pajamas ].freeze CI_ONLY_RULES ||= %w[ @@ -22,6 +23,7 @@ class GitlabDanger roulette ce_ee_vue_templates sidekiq_queues + specialization_labels ].freeze MESSAGE_PREFIX = '==>'.freeze diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index eea7daa3d8e..b349b46dc18 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -41,7 +41,7 @@ module Mattermost begin yield self rescue Errno::ECONNREFUSED => e - Rails.logger.error(e.message + "\n" + e.backtrace.join("\n")) # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.error(e.message + "\n" + e.backtrace.join("\n")) raise Mattermost::NoSessionError ensure destroy @@ -52,7 +52,7 @@ module Mattermost # Next methods are needed for Doorkeeper def pre_auth @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new( - Doorkeeper.configuration, server.client_via_uid, params) + Doorkeeper.configuration, params) end def authorization diff --git a/lib/microsoft_teams/notifier.rb b/lib/microsoft_teams/notifier.rb index 096e1e2ee96..0b21c355a54 100644 --- a/lib/microsoft_teams/notifier.rb +++ b/lib/microsoft_teams/notifier.rb @@ -19,7 +19,7 @@ module MicrosoftTeams result = true if response rescue Gitlab::HTTP::Error, StandardError => error - Rails.logger.info("#{self.class.name}: Error while connecting to #{@webhook}: #{error.message}") # rubocop:disable Gitlab/RailsLogger + Gitlab::AppLogger.info("#{self.class.name}: Error while connecting to #{@webhook}: #{error.message}") end result diff --git a/lib/object_storage/config.rb b/lib/object_storage/config.rb index d0777914cb5..cc536ce9b46 100644 --- a/lib/object_storage/config.rb +++ b/lib/object_storage/config.rb @@ -2,12 +2,26 @@ module ObjectStorage class Config + AWS_PROVIDER = 'AWS' + AZURE_PROVIDER = 'AzureRM' + GOOGLE_PROVIDER = 'Google' + attr_reader :options def initialize(options) @options = options.to_hash.deep_symbolize_keys end + def load_provider + if aws? + require 'fog/aws' + elsif google? + require 'fog/google' + elsif azure? + require 'fog/azurerm' + end + end + def credentials @credentials ||= options[:connection] || {} end @@ -30,7 +44,7 @@ module ObjectStorage # AWS-specific options def aws? - provider == 'AWS' + provider == AWS_PROVIDER end def use_iam_profile? @@ -54,12 +68,18 @@ module ObjectStorage end # End AWS-specific options + # Begin Azure-specific options + def azure_storage_domain + credentials[:azure_storage_domain] + end + # End Azure-specific options + def google? - provider == 'Google' + provider == GOOGLE_PROVIDER end def azure? - provider == 'AzureRM' + provider == AZURE_PROVIDER end def fog_attributes diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb index 90199114f2c..b5864382299 100644 --- a/lib/object_storage/direct_upload.rb +++ b/lib/object_storage/direct_upload.rb @@ -99,12 +99,18 @@ module ObjectStorage ObjectStorage: { Provider: 'AzureRM', GoCloudConfig: { - URL: "azblob://#{bucket_name}" + URL: azure_gocloud_url } } } end + def azure_gocloud_url + url = "azblob://#{bucket_name}" + url += "?domain=#{config.azure_storage_domain}" if config.azure_storage_domain.present? + url + end + def use_workhorse_s3_client? return false unless Feature.enabled?(:use_workhorse_s3_client, default_enabled: true) return false unless config.use_iam_profile? || config.consolidated_settings? diff --git a/lib/product_analytics/tracker.rb b/lib/product_analytics/tracker.rb index d4a88b879f0..2dc5e1f53ce 100644 --- a/lib/product_analytics/tracker.rb +++ b/lib/product_analytics/tracker.rb @@ -7,5 +7,36 @@ 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/system_check/app/redis_version_check.rb b/lib/system_check/app/redis_version_check.rb index aff0ee52e0d..697ced3590b 100644 --- a/lib/system_check/app/redis_version_check.rb +++ b/lib/system_check/app/redis_version_check.rb @@ -37,7 +37,7 @@ module SystemCheck @custom_error_message ) for_more_information( - 'doc/administration/high_availability/redis.md#provide-your-own-redis-instance-core-only' + 'doc/administration/high_availability/redis.md#provide-your-own-redis-instance' ) fix_and_rerun end diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake index 31030d061f2..1e28d15f75e 100644 --- a/lib/tasks/gettext.rake +++ b/lib/tasks/gettext.rake @@ -38,7 +38,9 @@ namespace :gettext do Rake::Task['gettext:find'].invoke # leave only the required changes. - `git checkout -- locale/*/gitlab.po` + unless system(*%w(git checkout -- locale/*/gitlab.po)) + raise 'failed to cleanup generated locale/*/gitlab.po files' + end # Remove timestamps from the pot file pot_content = File.read pot_file diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index 69a3526b872..caa583fb3a9 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -55,6 +55,7 @@ namespace :gitlab do rake:assets:precompile gitlab:assets:compile_webpack_if_needed gitlab:assets:fix_urls + gitlab:assets:check_page_bundle_mixins_css_for_sideeffects ].each(&::Gitlab::TaskHelpers.method(:invoke_and_time_task)) end @@ -127,5 +128,10 @@ namespace :gitlab do abort 'Error: Unable to compile webpack DLL.'.color(:red) end end + + desc 'GitLab | Assets | Check that scss mixins do not introduce any sideffects' + task :check_page_bundle_mixins_css_for_sideeffects do + system('./scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js') + end end end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index b0f1ca39387..2a3713ed85c 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -47,6 +47,11 @@ namespace :gitlab do begin unless ENV['force'] == 'yes' warning = <<-MSG.strip_heredoc + Be sure to stop Puma, Sidekiq, and any other process that + connects to the database before proceeding. For Omnibus + installs, see the following link for more information: + https://docs.gitlab.com/ee/raketasks/backup_restore.html#restore-for-omnibus-gitlab-installations + Before restoring the database, we will remove all existing tables to avoid future upgrade problems. Be aware that if you have custom tables in the GitLab database these tables and all data will be @@ -131,7 +136,21 @@ namespace :gitlab do task restore: :gitlab_environment do puts_time "Restoring database ... ".color(:blue) - Backup::Database.new(progress).restore + errors = Backup::Database.new(progress).restore + + if errors.present? + warning = <<~MSG + There were errors in restoring the schema. This may cause + issues if this results in missing indexes, constraints, or + columns. Please record the errors above and contact GitLab + Support if you have questions: + https://about.gitlab.com/support/ + MSG + + warn warning.color(:red) + ask_to_continue + end + puts_time "done".color(:green) end end @@ -273,5 +292,7 @@ namespace :gitlab do $stdout end end - end # namespace end: backup -end # namespace end: gitlab + end + # namespace end: backup +end +# namespace end: gitlab diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 61318570fd5..425f66918b0 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -166,5 +166,12 @@ namespace :gitlab do Rake::Task['db:test:load'].enhance do Rake::Task['gitlab:db:create_dynamic_partitions'].invoke end + + desc 'reindex a regular (non-unique) index without downtime to eliminate bloat' + task :reindex, [:index_name] => :environment do |_, args| + raise ArgumentError, 'must give the index name to reindex' unless args[:index_name] + + Gitlab::Database::ConcurrentReindex.new(args[:index_name], logger: Logger.new(STDOUT)).execute + end end end diff --git a/lib/tasks/gitlab/lfs/migrate.rake b/lib/tasks/gitlab/lfs/migrate.rake index 470a12c39cd..3d4c847a0f0 100644 --- a/lib/tasks/gitlab/lfs/migrate.rake +++ b/lib/tasks/gitlab/lfs/migrate.rake @@ -9,12 +9,12 @@ namespace :gitlab do LfsObject.with_files_stored_locally .find_each(batch_size: 10) do |lfs_object| - lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) + lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) - logger.info("Transferred LFS object #{lfs_object.oid} of size #{lfs_object.size.to_i.bytes} to object storage") - rescue => e - logger.error("Failed to transfer LFS object #{lfs_object.oid} with error: #{e.message}") - end + logger.info("Transferred LFS object #{lfs_object.oid} of size #{lfs_object.size.to_i.bytes} to object storage") + rescue => e + logger.error("Failed to transfer LFS object #{lfs_object.oid} with error: #{e.message}") + end end task migrate_to_local: :environment do diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake index eb3de195626..6f00db42d78 100644 --- a/lib/tasks/gitlab/sidekiq.rake +++ b/lib/tasks/gitlab/sidekiq.rake @@ -5,7 +5,7 @@ return if Rails.env.production? namespace :gitlab do namespace :sidekiq do def write_yaml(path, banner, object) - File.write(path, banner + YAML.dump(object)) + File.write(path, banner + YAML.dump(object).gsub(/ *$/m, '')) end namespace :all_queues_yml do diff --git a/lib/tasks/gitlab/usage_data.rake b/lib/tasks/gitlab/usage_data.rake new file mode 100644 index 00000000000..6f3db91c2b0 --- /dev/null +++ b/lib/tasks/gitlab/usage_data.rake @@ -0,0 +1,13 @@ +namespace :gitlab do + namespace :usage_data do + desc 'GitLab | UsageData | Generate raw SQLs for usage ping in YAML' + task dump_sql_in_yaml: :environment do + puts Gitlab::UsageDataQueries.uncached_data.to_yaml + end + + desc 'GitLab | UsageData | Generate raw SQLs for usage ping in JSON' + task dump_sql_in_json: :environment do + puts Gitlab::Json.pretty_generate(Gitlab::UsageDataQueries.uncached_data) + end + end +end diff --git a/lib/tasks/pngquant.rake b/lib/tasks/pngquant.rake index ceb4de55373..63bc1c7c16e 100644 --- a/lib/tasks/pngquant.rake +++ b/lib/tasks/pngquant.rake @@ -2,10 +2,10 @@ return if Rails.env.production? require 'png_quantizator' require 'parallel' +require_relative '../../tooling/lib/tooling/images' # The amount of variance (in bytes) allowed in # file size when testing for compression size -TOLERANCE = 10000 namespace :pngquant do # Returns an array of all images eligible for compression @@ -13,55 +13,13 @@ namespace :pngquant do Dir.glob('doc/**/*.png', File::FNM_CASEFOLD) end - # Runs pngquant on an image and optionally - # writes the result to disk - def compress_image(file, overwrite_original) - compressed_file = "#{file}.compressed" - FileUtils.copy(file, compressed_file) - - pngquant_file = PngQuantizator::Image.new(compressed_file) - - # Run the image repeatedly through pngquant until - # the change in file size is within TOLERANCE - loop do - before = File.size(compressed_file) - pngquant_file.quantize! - after = File.size(compressed_file) - break if before - after <= TOLERANCE - end - - savings = File.size(file) - File.size(compressed_file) - is_uncompressed = savings > TOLERANCE - - if is_uncompressed && overwrite_original - FileUtils.copy(compressed_file, file) - end - - FileUtils.remove(compressed_file) - - [is_uncompressed, savings] - end - - # Ensures pngquant is available and prints an error if not - def check_executable - unless system('pngquant --version', out: File::NULL) - warn( - 'Error: pngquant executable was not detected in the system.'.color(:red), - 'Download pngquant at https://pngquant.org/ and place the executable in /usr/local/bin'.color(:green) - ) - abort - end - end - desc 'GitLab | Pngquant | Compress all documentation PNG images using pngquant' task :compress do - check_executable - files = doc_images puts "Compressing #{files.size} PNG files in doc/**" Parallel.each(files) do |file| - was_uncompressed, savings = compress_image(file, true) + was_uncompressed, savings = Tooling::Image.compress_image(file) if was_uncompressed puts "#{file} was reduced by #{savings} bytes" @@ -71,13 +29,11 @@ namespace :pngquant do desc 'GitLab | Pngquant | Checks that all documentation PNG images have been compressed with pngquant' task :lint do - check_executable - files = doc_images puts "Checking #{files.size} PNG files in doc/**" uncompressed_files = Parallel.map(files) do |file| - is_uncompressed, _ = compress_image(file, false) + is_uncompressed, _ = Tooling::Image.compress_image(file, true) if is_uncompressed puts "Uncompressed file detected: ".color(:red) + file file diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb index 73029c934f4..cd5943b552e 100644 --- a/lib/uploaded_file.rb +++ b/lib/uploaded_file.rb @@ -42,6 +42,32 @@ class UploadedFile @remote_id = remote_id end + def self.from_params_without_field(params, upload_paths) + path = params['path'] + remote_id = params['remote_id'] + return if path.blank? && remote_id.blank? + + # don't use file_path if remote_id is set + if remote_id.present? + file_path = nil + elsif path.present? + file_path = File.realpath(path) + + unless self.allowed_path?(file_path, Array(upload_paths).compact) + raise InvalidPathError, "insecure path used '#{file_path}'" + end + end + + UploadedFile.new( + file_path, + filename: params['name'], + content_type: params['type'] || 'application/octet-stream', + sha256: params['sha256'], + remote_id: remote_id, + size: params['size'] + ) + end + def self.from_params(params, field, upload_paths, path_override = nil) path = path_override || params["#{field}.path"] remote_id = params["#{field}.remote_id"] @@ -52,8 +78,7 @@ class UploadedFile elsif path.present? file_path = File.realpath(path) - paths = Array(upload_paths) << Dir.tmpdir - unless self.allowed_path?(file_path, paths.compact) + unless self.allowed_path?(file_path, Array(upload_paths).compact) raise InvalidPathError, "insecure path used '#{file_path}'" end end |