diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-20 15:52:10 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-20 15:52:10 +0300 |
commit | dba864470fbcbb6bdd5b94eb510acdce62c962d8 (patch) | |
tree | e8ead0b84e7b814f5891d2c8cd3db2d6b635fb64 /lib | |
parent | b7d29500f28ff59c8898cdf889a40d3da908f162 (diff) |
Add latest changes from gitlab-org/gitlab@12-8-stable-ee
Diffstat (limited to 'lib')
473 files changed, 8291 insertions, 3424 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb index 1aee4fd30ee..9a1e0e3f8e9 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -47,7 +47,8 @@ module API Gitlab::ApplicationContext.push( user: -> { current_user }, project: -> { @project }, - namespace: -> { @group } + namespace: -> { @group }, + caller_id: route.origin ) end @@ -102,94 +103,103 @@ module API helpers ::API::Helpers helpers ::API::Helpers::CommonHelpers - # Keep in alphabetical order - mount ::API::AccessRequests - mount ::API::Appearance - mount ::API::Applications - mount ::API::Avatar - mount ::API::AwardEmoji - mount ::API::Badges - mount ::API::Boards - mount ::API::Branches - mount ::API::BroadcastMessages - mount ::API::Commits - mount ::API::CommitStatuses - mount ::API::DeployKeys - mount ::API::Deployments - mount ::API::Environments - mount ::API::ErrorTracking - mount ::API::Events - mount ::API::Features - mount ::API::Files - mount ::API::GroupBoards - mount ::API::GroupClusters - mount ::API::GroupExport - mount ::API::GroupLabels - mount ::API::GroupMilestones - mount ::API::Groups - mount ::API::GroupContainerRepositories - mount ::API::GroupVariables - mount ::API::ImportGithub + namespace do + after do + ::Users::ActivityService.new(@current_user).execute if Feature.enabled?(:api_activity_logging) + end + + # Keep in alphabetical order + mount ::API::AccessRequests + mount ::API::Appearance + mount ::API::Applications + mount ::API::Avatar + mount ::API::AwardEmoji + mount ::API::Badges + mount ::API::Boards + mount ::API::Branches + mount ::API::BroadcastMessages + mount ::API::Commits + mount ::API::CommitStatuses + mount ::API::DeployKeys + mount ::API::Deployments + mount ::API::Environments + mount ::API::ErrorTracking + mount ::API::Events + mount ::API::Features + mount ::API::Files + mount ::API::GroupBoards + mount ::API::GroupClusters + mount ::API::GroupExport + mount ::API::GroupImport + mount ::API::GroupLabels + mount ::API::GroupMilestones + mount ::API::Groups + mount ::API::GroupContainerRepositories + mount ::API::GroupVariables + mount ::API::ImportGithub + mount ::API::Issues + mount ::API::JobArtifacts + mount ::API::Jobs + mount ::API::Keys + mount ::API::Labels + mount ::API::Lint + mount ::API::LsifData + mount ::API::Markdown + mount ::API::Members + mount ::API::MergeRequestDiffs + mount ::API::MergeRequests + mount ::API::Namespaces + mount ::API::Notes + mount ::API::Discussions + mount ::API::ResourceLabelEvents + mount ::API::NotificationSettings + mount ::API::Pages + mount ::API::PagesDomains + mount ::API::Pipelines + mount ::API::PipelineSchedules + mount ::API::ProjectClusters + mount ::API::ProjectContainerRepositories + mount ::API::ProjectEvents + mount ::API::ProjectExport + mount ::API::ProjectImport + mount ::API::ProjectHooks + mount ::API::ProjectMilestones + mount ::API::Projects + mount ::API::ProjectSnapshots + mount ::API::ProjectSnippets + mount ::API::ProjectStatistics + mount ::API::ProjectTemplates + mount ::API::ProtectedBranches + mount ::API::ProtectedTags + mount ::API::Releases + mount ::API::Release::Links + mount ::API::RemoteMirrors + mount ::API::Repositories + mount ::API::Runner + mount ::API::Runners + mount ::API::Search + mount ::API::Services + mount ::API::Settings + mount ::API::SidekiqMetrics + mount ::API::Snippets + mount ::API::Statistics + mount ::API::Submodules + mount ::API::Subscriptions + mount ::API::Suggestions + mount ::API::SystemHooks + mount ::API::Tags + mount ::API::Templates + mount ::API::Todos + mount ::API::Triggers + mount ::API::UserCounts + mount ::API::Users + mount ::API::Variables + mount ::API::Version + mount ::API::Wikis + end + mount ::API::Internal::Base mount ::API::Internal::Pages - mount ::API::Issues - mount ::API::JobArtifacts - mount ::API::Jobs - mount ::API::Keys - mount ::API::Labels - mount ::API::Lint - mount ::API::Markdown - mount ::API::Members - mount ::API::MergeRequestDiffs - mount ::API::MergeRequests - mount ::API::Namespaces - mount ::API::Notes - mount ::API::Discussions - mount ::API::ResourceLabelEvents - mount ::API::NotificationSettings - mount ::API::Pages - mount ::API::PagesDomains - mount ::API::Pipelines - mount ::API::PipelineSchedules - mount ::API::ProjectClusters - mount ::API::ProjectContainerRepositories - mount ::API::ProjectEvents - mount ::API::ProjectExport - mount ::API::ProjectImport - mount ::API::ProjectHooks - mount ::API::ProjectMilestones - mount ::API::Projects - mount ::API::ProjectSnapshots - mount ::API::ProjectSnippets - mount ::API::ProjectStatistics - mount ::API::ProjectTemplates - mount ::API::ProtectedBranches - mount ::API::ProtectedTags - mount ::API::Releases - mount ::API::Release::Links - mount ::API::RemoteMirrors - mount ::API::Repositories - mount ::API::Runner - mount ::API::Runners - mount ::API::Search - mount ::API::Services - mount ::API::Settings - mount ::API::SidekiqMetrics - mount ::API::Snippets - mount ::API::Statistics - mount ::API::Submodules - mount ::API::Subscriptions - mount ::API::Suggestions - mount ::API::SystemHooks - mount ::API::Tags - mount ::API::Templates - mount ::API::Todos - mount ::API::Triggers - mount ::API::UserCounts - mount ::API::Users - mount ::API::Variables - mount ::API::Version - mount ::API::Wikis route :any, '*path' do error!('404 Not Found', 404) diff --git a/lib/api/applications.rb b/lib/api/applications.rb index 4e9843e17e8..70e6b8395d7 100644 --- a/lib/api/applications.rb +++ b/lib/api/applications.rb @@ -14,6 +14,9 @@ module API requires :name, type: String, desc: 'Application name' requires :redirect_uri, type: String, desc: 'Application redirect URI' requires :scopes, type: String, desc: 'Application scopes' + + optional :confidential, type: Boolean, default: true, + desc: 'Application will be used where the client secret is confidential' end post do application = Doorkeeper::Application.new(declared_params) diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 7a815fa3dde..8e3b3ff8ce5 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -127,6 +127,8 @@ module API case awardable when Note read_ability(awardable.noteable) + when Snippet, ProjectSnippet + :read_snippet else :"read_#{awardable.class.to_s.underscore}" end diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index 994e12445b7..af7c69f857e 100644 --- a/lib/api/broadcast_messages.rb +++ b/lib/api/broadcast_messages.rb @@ -4,9 +4,6 @@ module API class BroadcastMessages < Grape::API include PaginationParams - before { authenticate! } - before { authenticated_as_admin! } - resource :broadcast_messages do helpers do def find_message @@ -38,8 +35,11 @@ module API optional :color, type: String, desc: 'Background color' optional :font, type: String, desc: 'Foreground color' optional :target_path, type: String, desc: 'Target path' + optional :broadcast_type, type: String, values: BroadcastMessage.broadcast_types.keys, desc: 'Broadcast type. Defaults to banner', default: -> { 'banner' } end post do + authenticated_as_admin! + message = BroadcastMessage.create(declared_params(include_missing: false)) if message.persisted? @@ -74,8 +74,11 @@ module API optional :color, type: String, desc: 'Background color' optional :font, type: String, desc: 'Foreground color' optional :target_path, type: String, desc: 'Target path' + optional :broadcast_type, type: String, values: BroadcastMessage.broadcast_types.keys, desc: 'Broadcast Type' end put ':id' do + authenticated_as_admin! + message = find_message if message.update(declared_params(include_missing: false)) @@ -93,6 +96,8 @@ module API requires :id, type: Integer, desc: 'Broadcast message ID' end delete ':id' do + authenticated_as_admin! + message = find_message destroy_conditionally!(message) diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 569c4e04dc5..b4c5d7869a2 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -76,14 +76,12 @@ module API name = params[:name] || params[:context] || 'default' - unless pipeline - pipeline = user_project.ci_pipelines.create!( - source: :external, - sha: commit.sha, - ref: ref, - user: current_user, - protected: user_project.protected_for?(ref)) - end + pipeline ||= user_project.ci_pipelines.create!( + source: :external, + sha: commit.sha, + ref: ref, + user: current_user, + protected: user_project.protected_for?(ref)) authorize! :update_pipeline, pipeline diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 9dcf9b015aa..dfb0066ceb0 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -38,6 +38,7 @@ module API optional :all, type: Boolean, desc: 'Every commit will be returned' optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response' optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges' + optional :order, type: String, desc: 'List commits in order', default: 'default', values: %w[default topo] use :pagination end get ':id/repository/commits' do @@ -49,6 +50,7 @@ module API all = params[:all] with_stats = params[:with_stats] first_parent = params[:first_parent] + order = params[:order] commits = user_project.repository.commits(ref, path: path, @@ -57,7 +59,8 @@ module API before: before, after: after, all: all, - first_parent: first_parent) + first_parent: first_parent, + order: order) commit_count = if all || path || before || after || first_parent diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 9125207167c..25d38615c7f 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -26,7 +26,7 @@ module API end get ":id/#{noteables_path}/:noteable_id/discussions" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) notes = readable_discussion_notes(noteable) discussions = Kaminari.paginate_array(Discussion.build_collection(notes, noteable)) @@ -42,7 +42,7 @@ module API requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable' end get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) notes = readable_discussion_notes(noteable, params[:discussion_id]) if notes.empty? @@ -77,7 +77,7 @@ module API end end post ":id/#{noteables_path}/:noteable_id/discussions" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) type = params[:position] ? 'DiffNote' : 'DiscussionNote' id_key = noteable.is_a?(Commit) ? :commit_id : :noteable_id @@ -107,7 +107,7 @@ module API requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable' end get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) notes = readable_discussion_notes(noteable, params[:discussion_id]) if notes.empty? @@ -127,7 +127,7 @@ module API optional :created_at, type: String, desc: 'The creation date of the note' end post ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) notes = readable_discussion_notes(noteable, params[:discussion_id]) first_note = notes.first @@ -161,7 +161,7 @@ module API requires :note_id, type: Integer, desc: 'The ID of a note' end get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) get_note(noteable, params[:note_id]) end @@ -178,7 +178,7 @@ module API exactly_one_of :body, :resolved end put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) if params[:resolved].nil? update_note(noteable, params[:note_id]) @@ -196,7 +196,7 @@ module API requires :note_id, type: Integer, desc: 'The ID of a note' end delete ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) delete_note(noteable, params[:note_id]) end @@ -211,7 +211,7 @@ module API requires :resolved, type: Boolean, desc: 'Mark discussion resolved/unresolved' end put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) resolve_discussion(noteable, params[:discussion_id], params[:resolved]) end diff --git a/lib/api/entities.rb b/lib/api/entities.rb deleted file mode 100644 index 8c4d986eb34..00000000000 --- a/lib/api/entities.rb +++ /dev/null @@ -1,1966 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class BlameRangeCommit < Grape::Entity - expose :id - expose :parent_ids - expose :message - expose :authored_date, :author_name, :author_email - expose :committed_date, :committer_name, :committer_email - end - - class BlameRange < Grape::Entity - expose :commit, using: BlameRangeCommit - expose :lines - end - - class WikiPageBasic < Grape::Entity - expose :format - expose :slug - expose :title - end - - class WikiPage < WikiPageBasic - expose :content - end - - class WikiAttachment < Grape::Entity - include Gitlab::FileMarkdownLinkBuilder - - expose :file_name - expose :file_path - expose :branch - expose :link do - expose :file_path, as: :url - expose :markdown do |_entity| - self.markdown_link - end - end - - def filename - object.file_name - end - - def secure_url - object.file_path - end - end - - class UserSafe < Grape::Entity - expose :id, :name, :username - end - - class UserBasic < UserSafe - expose :state - - expose :avatar_url do |user, options| - user.avatar_url(only_path: false) - end - - expose :avatar_path, if: ->(user, options) { options.fetch(:only_path, false) && user.avatar_path } - expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes - - expose :web_url do |user, options| - Gitlab::Routing.url_helpers.user_url(user) - end - end - - class User < UserBasic - expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } - expose :bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization - end - - class UserActivity < Grape::Entity - expose :username - expose :last_activity_on - expose :last_activity_on, as: :last_activity_at # Back-compat - end - - class UserStarsProject < Grape::Entity - expose :starred_since - expose :user, using: Entities::UserBasic - end - - class Identity < Grape::Entity - expose :provider, :extern_uid - end - - class UserPublic < User - expose :last_sign_in_at - expose :confirmed_at - expose :last_activity_on - expose :email - expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at - expose :identities, using: Entities::Identity - expose :can_create_group?, as: :can_create_group - expose :can_create_project?, as: :can_create_project - expose :two_factor_enabled?, as: :two_factor_enabled - expose :external - expose :private_profile - end - - class UserWithAdmin < UserPublic - expose :admin?, as: :is_admin - end - - class UserDetailsWithAdmin < UserWithAdmin - expose :highest_role - end - - class UserStatus < Grape::Entity - expose :emoji - expose :message - expose :message_html do |entity| - MarkupHelper.markdown_field(entity, :message) - end - end - - class Email < Grape::Entity - expose :id, :email - end - - class Hook < Grape::Entity - expose :id, :url, :created_at, :push_events, :tag_push_events, :merge_requests_events, :repository_update_events - expose :enable_ssl_verification - end - - class ProjectHook < Hook - expose :project_id, :issues_events, :confidential_issues_events - expose :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events - expose :job_events - expose :push_events_branch_filter - end - - class SharedGroup < Grape::Entity - expose :group_id - expose :group_name do |group_link, options| - group_link.group.name - end - expose :group_full_path do |group_link, options| - group_link.group.full_path - end - expose :group_access, as: :group_access_level - expose :expires_at - end - - class ProjectIdentity < Grape::Entity - expose :id, :description - expose :name, :name_with_namespace - expose :path, :path_with_namespace - expose :created_at - end - - class ProjectExportStatus < ProjectIdentity - include ::API::Helpers::RelatedResourcesHelpers - - expose :export_status - expose :_links, if: lambda { |project, _options| project.export_status == :finished } do - expose :api_url do |project| - expose_url(api_v4_projects_export_download_path(id: project.id)) - end - - expose :web_url do |project| - Gitlab::Routing.url_helpers.download_export_project_url(project) - end - end - end - - class RemoteMirror < Grape::Entity - expose :id - expose :enabled - expose :safe_url, as: :url - expose :update_status - expose :last_update_at - expose :last_update_started_at - expose :last_successful_update_at - expose :last_error - expose :only_protected_branches - end - - class ContainerExpirationPolicy < Grape::Entity - expose :cadence - expose :enabled - expose :keep_n - expose :older_than - expose :name_regex - expose :next_run_at - end - - class ProjectImportStatus < ProjectIdentity - expose :import_status - - # TODO: Use `expose_nil` once we upgrade the grape-entity gem - expose :import_error, if: lambda { |project, _ops| project.import_state&.last_error } do |project| - project.import_state.last_error - end - end - - class BasicProjectDetails < ProjectIdentity - include ::API::ProjectsRelationBuilder - - expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } - # Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770 - expose :tag_list do |project| - # project.tags.order(:name).pluck(:name) is the most suitable option - # to avoid loading all the ActiveRecord objects but, if we use it here - # it override the preloaded associations and makes a query - # (fixed in https://github.com/rails/rails/pull/25976). - project.tags.map(&:name).sort - end - - expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url - - expose :license_url, if: :license do |project| - license = project.repository.license_blob - - if license - Gitlab::Routing.url_helpers.project_blob_url(project, File.join(project.default_branch, license.path)) - end - end - - expose :license, with: 'API::Entities::LicenseBasic', if: :license do |project| - project.repository.license - end - - expose :avatar_url do |project, options| - project.avatar_url(only_path: false) - end - - expose :star_count, :forks_count - expose :last_activity_at - expose :namespace, using: 'API::Entities::NamespaceBasic' - expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes - - # rubocop: disable CodeReuse/ActiveRecord - def self.preload_relation(projects_relation, options = {}) - # Preloading tags, should be done with using only `:tags`, - # as `:tags` are defined as: `has_many :tags, through: :taggings` - # N+1 is solved then by using `subject.tags.map(&:name)` - # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/20555 - projects_relation.preload(:project_feature, :route) - .preload(:import_state, :tags) - .preload(:auto_devops) - .preload(namespace: [:route, :owner]) - end - # rubocop: enable CodeReuse/ActiveRecord - end - - class Project < BasicProjectDetails - include ::API::Helpers::RelatedResourcesHelpers - - expose :_links do - expose :self do |project| - expose_url(api_v4_projects_path(id: project.id)) - end - - expose :issues, if: -> (project, options) { issues_available?(project, options) } do |project| - expose_url(api_v4_projects_issues_path(id: project.id)) - end - - expose :merge_requests, if: -> (project, options) { mrs_available?(project, options) } do |project| - expose_url(api_v4_projects_merge_requests_path(id: project.id)) - end - - expose :repo_branches do |project| - expose_url(api_v4_projects_repository_branches_path(id: project.id)) - end - - expose :labels do |project| - expose_url(api_v4_projects_labels_path(id: project.id)) - end - - expose :events do |project| - expose_url(api_v4_projects_events_path(id: project.id)) - end - - expose :members do |project| - expose_url(api_v4_projects_members_path(id: project.id)) - end - end - - expose :empty_repo?, as: :empty_repo - expose :archived?, as: :archived - expose :visibility - expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } - expose :resolve_outdated_diff_discussions - expose :container_registry_enabled - expose :container_expiration_policy, using: Entities::ContainerExpirationPolicy, - if: -> (project, _) { project.container_expiration_policy } - - # Expose old field names with the new permissions methods to keep API compatible - # TODO: remove in API v5, replaced by *_access_level - expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } - expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } - expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } - expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } - expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } - - expose(:issues_access_level) { |project, options| project.project_feature.string_access_level(:issues) } - expose(:repository_access_level) { |project, options| project.project_feature.string_access_level(:repository) } - expose(:merge_requests_access_level) { |project, options| project.project_feature.string_access_level(:merge_requests) } - expose(:wiki_access_level) { |project, options| project.project_feature.string_access_level(:wiki) } - expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) } - expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) } - - expose :shared_runners_enabled - expose :lfs_enabled?, as: :lfs_enabled - expose :creator_id - expose :forked_from_project, using: Entities::BasicProjectDetails, if: ->(project, options) do - project.forked? && Ability.allowed?(options[:current_user], :read_project, project.forked_from_project) - end - expose :import_status - - expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] } do |project| - project.import_state&.last_error - end - - expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } - expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } - expose :ci_default_git_depth - expose :public_builds, as: :public_jobs - expose :build_git_strategy, if: lambda { |project, options| options[:user_can_admin_project] } do |project, options| - project.build_allow_git_fetch ? 'fetch' : 'clone' - end - expose :build_timeout - expose :auto_cancel_pending_pipelines - expose :build_coverage_regex - expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } - expose :shared_with_groups do |project, options| - SharedGroup.represent(project.project_group_links, options) - end - expose :only_allow_merge_if_pipeline_succeeds - expose :request_access_enabled - expose :only_allow_merge_if_all_discussions_are_resolved - expose :remove_source_branch_after_merge - expose :printing_merge_request_link_enabled - expose :merge_method - expose :suggestion_commit_message - expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) { - options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project) - } - expose :auto_devops_enabled?, as: :auto_devops_enabled - expose :auto_devops_deploy_strategy do |project, options| - project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy - end - expose :autoclose_referenced_issues - - # rubocop: disable CodeReuse/ActiveRecord - def self.preload_relation(projects_relation, options = {}) - # Preloading tags, should be done with using only `:tags`, - # as `:tags` are defined as: `has_many :tags, through: :taggings` - # N+1 is solved then by using `subject.tags.map(&:name)` - # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/20555 - super(projects_relation).preload(:group) - .preload(:ci_cd_settings) - .preload(:container_expiration_policy) - .preload(:auto_devops) - .preload(project_group_links: { group: :route }, - fork_network: :root_project, - fork_network_member: :forked_from_project, - forked_from_project: [:route, :forks, :tags, namespace: :route]) - end - # rubocop: enable CodeReuse/ActiveRecord - - def self.forks_counting_projects(projects_relation) - projects_relation + projects_relation.map(&:forked_from_project).compact - end - end - - class ProjectStatistics < Grape::Entity - expose :commit_count - expose :storage_size - expose :repository_size - expose :wiki_size - expose :lfs_objects_size - expose :build_artifacts_size, as: :job_artifacts_size - end - - class ProjectDailyFetches < Grape::Entity - expose :fetch_count, as: :count - expose :date - end - - class ProjectDailyStatistics < Grape::Entity - expose :fetches do - expose :total_fetch_count, as: :total - expose :fetches, as: :days, using: ProjectDailyFetches - end - end - - class Member < Grape::Entity - expose :user, merge: true, using: UserBasic - expose :access_level - expose :expires_at - end - - class AccessRequester < Grape::Entity - expose :user, merge: true, using: UserBasic - expose :requested_at - end - - class BasicGroupDetails < Grape::Entity - expose :id - expose :web_url - expose :name - end - - class Group < BasicGroupDetails - expose :path, :description, :visibility - expose :share_with_group_lock - expose :require_two_factor_authentication - expose :two_factor_grace_period - expose :project_creation_level_str, as: :project_creation_level - expose :auto_devops_enabled - expose :subgroup_creation_level_str, as: :subgroup_creation_level - expose :emails_disabled - expose :mentions_disabled - expose :lfs_enabled?, as: :lfs_enabled - expose :avatar_url do |group, options| - group.avatar_url(only_path: false) - end - expose :request_access_enabled - expose :full_name, :full_path - expose :parent_id - - expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes - - expose :statistics, if: :statistics do - with_options format_with: -> (value) { value.to_i } do - expose :storage_size - expose :repository_size - expose :wiki_size - expose :lfs_objects_size - expose :build_artifacts_size, as: :job_artifacts_size - end - end - end - - class GroupDetail < Group - expose :runners_token, if: lambda { |group, options| options[:user_can_admin_group] } - expose :projects, using: Entities::Project do |group, options| - projects = GroupProjectsFinder.new( - group: group, - current_user: options[:current_user], - options: { only_owned: true, limit: projects_limit } - ).execute - - Entities::Project.prepare_relation(projects) - end - - expose :shared_projects, using: Entities::Project do |group, options| - projects = GroupProjectsFinder.new( - group: group, - current_user: options[:current_user], - options: { only_shared: true, limit: projects_limit } - ).execute - - Entities::Project.prepare_relation(projects) - end - - def projects_limit - if ::Feature.enabled?(:limit_projects_in_groups_api, default_enabled: true) - GroupProjectsFinder::DEFAULT_PROJECTS_LIMIT - else - nil - end - end - end - - class DiffRefs < Grape::Entity - expose :base_sha, :head_sha, :start_sha - end - - class Commit < Grape::Entity - expose :id, :short_id, :created_at - expose :parent_ids - expose :full_title, as: :title - expose :safe_message, as: :message - expose :author_name, :author_email, :authored_date - expose :committer_name, :committer_email, :committed_date - end - - class CommitStats < Grape::Entity - expose :additions, :deletions, :total - end - - class CommitWithStats < Commit - expose :stats, using: Entities::CommitStats - end - - class CommitDetail < Commit - expose :stats, using: Entities::CommitStats, if: :stats - expose :status - expose :project_id - - expose :last_pipeline do |commit, options| - pipeline = commit.last_pipeline if can_read_pipeline? - ::API::Entities::PipelineBasic.represent(pipeline, options) - end - - private - - def can_read_pipeline? - Ability.allowed?(options[:current_user], :read_pipeline, object.last_pipeline) - end - end - - class CommitSignature < Grape::Entity - expose :gpg_key_id - expose :gpg_key_primary_keyid, :gpg_key_user_name, :gpg_key_user_email - expose :verification_status - expose :gpg_key_subkey_id - end - - class BasicRef < Grape::Entity - expose :type, :name - end - - class Branch < Grape::Entity - expose :name - - expose :commit, using: Entities::Commit do |repo_branch, options| - options[:project].repository.commit(repo_branch.dereferenced_target) - end - - expose :merged do |repo_branch, options| - if options[:merged_branch_names] - options[:merged_branch_names].include?(repo_branch.name) - else - options[:project].repository.merged_to_root_ref?(repo_branch) - end - end - - expose :protected do |repo_branch, options| - ::ProtectedBranch.protected?(options[:project], repo_branch.name) - end - - expose :developers_can_push do |repo_branch, options| - ::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches) - end - - expose :developers_can_merge do |repo_branch, options| - ::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches) - end - - expose :can_push do |repo_branch, options| - Gitlab::UserAccess.new(options[:current_user], project: options[:project]).can_push_to_branch?(repo_branch.name) - end - - expose :default do |repo_branch, options| - options[:project].default_branch == repo_branch.name - end - end - - class TreeObject < Grape::Entity - expose :id, :name, :type, :path - - expose :mode do |obj, options| - filemode = obj.mode - filemode = "0" + filemode if filemode.length < 6 - filemode - end - end - - class Snippet < Grape::Entity - expose :id, :title, :file_name, :description, :visibility - expose :author, using: Entities::UserBasic - expose :updated_at, :created_at - expose :project_id - expose :web_url do |snippet| - Gitlab::UrlBuilder.build(snippet) - end - end - - class ProjectSnippet < Snippet - end - - class PersonalSnippet < Snippet - expose :raw_url do |snippet| - Gitlab::UrlBuilder.build(snippet, raw: true) - end - end - - class IssuableEntity < Grape::Entity - expose :id, :iid - expose(:project_id) { |entity| entity&.project.try(:id) } - expose :title, :description - expose :state, :created_at, :updated_at - - # Avoids an N+1 query when metadata is included - def issuable_metadata(subject, options, method, args = nil) - cached_subject = options.dig(:issuable_metadata, subject.id) - (cached_subject || subject).public_send(method, *args) # rubocop: disable GitlabSecurity/PublicSend - end - end - - class IssuableReferences < Grape::Entity - expose :short do |issuable| - issuable.to_reference - end - - expose :relative do |issuable, options| - issuable.to_reference(options[:group] || options[:project]) - end - - expose :full do |issuable| - issuable.to_reference(full: true) - end - end - - class Diff < Grape::Entity - expose :old_path, :new_path, :a_mode, :b_mode - expose :new_file?, as: :new_file - expose :renamed_file?, as: :renamed_file - expose :deleted_file?, as: :deleted_file - expose :json_safe_diff, as: :diff - end - - class ProtectedRefAccess < Grape::Entity - expose :access_level - expose :access_level_description do |protected_ref_access| - protected_ref_access.humanize - end - end - - class ProtectedBranch < Grape::Entity - expose :id - expose :name - expose :push_access_levels, using: Entities::ProtectedRefAccess - expose :merge_access_levels, using: Entities::ProtectedRefAccess - end - - class ProtectedTag < Grape::Entity - expose :name - expose :create_access_levels, using: Entities::ProtectedRefAccess - end - - class Milestone < Grape::Entity - expose :id, :iid - expose :project_id, if: -> (entity, options) { entity&.project_id } - expose :group_id, if: -> (entity, options) { entity&.group_id } - expose :title, :description - expose :state, :created_at, :updated_at - expose :due_date - expose :start_date - - expose :web_url do |milestone, _options| - Gitlab::UrlBuilder.build(milestone) - end - end - - class IssueBasic < IssuableEntity - expose :closed_at - expose :closed_by, using: Entities::UserBasic - - expose :labels do |issue, options| - if options[:with_labels_details] - ::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title)) - else - issue.labels.map(&:title).sort - end - end - - expose :milestone, using: Entities::Milestone - expose :assignees, :author, using: Entities::UserBasic - - expose :assignee, using: ::API::Entities::UserBasic do |issue| - issue.assignees.first - end - - expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) } - expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count, options[:current_user]) } - expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) } - expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) } - expose :due_date - expose :confidential - expose :discussion_locked - - expose :web_url do |issue| - Gitlab::UrlBuilder.build(issue) - end - - expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |issue| - issue - end - - expose :task_completion_status - end - - class Issue < IssueBasic - include ::API::Helpers::RelatedResourcesHelpers - - expose(:has_tasks) do |issue, _| - !issue.task_list_items.empty? - end - - expose :task_status, if: -> (issue, _) do - !issue.task_list_items.empty? - end - - expose :_links do - expose :self do |issue| - expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid)) - end - - expose :notes do |issue| - expose_url(api_v4_projects_issues_notes_path(id: issue.project_id, noteable_id: issue.iid)) - end - - expose :award_emoji do |issue| - expose_url(api_v4_projects_issues_award_emoji_path(id: issue.project_id, issue_iid: issue.iid)) - end - - expose :project do |issue| - expose_url(api_v4_projects_path(id: issue.project_id)) - end - end - - expose :references, with: IssuableReferences do |issue| - issue - end - - # Calculating the value of subscribed field triggers Markdown - # processing. We can't do that for multiple issues / merge - # requests in a single API request. - expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |issue, options| - issue.subscribed?(options[:current_user], options[:project] || issue.project) - end - - expose :moved_to_id - end - - class IssuableTimeStats < Grape::Entity - format_with(:time_tracking_formatter) do |time_spent| - Gitlab::TimeTrackingFormatter.output(time_spent) - end - - expose :time_estimate - expose :total_time_spent - expose :human_time_estimate - - with_options(format_with: :time_tracking_formatter) do - expose :total_time_spent, as: :human_total_time_spent - end - - # rubocop: disable CodeReuse/ActiveRecord - def total_time_spent - # Avoids an N+1 query since timelogs are preloaded - object.timelogs.map(&:time_spent).sum - end - # rubocop: enable CodeReuse/ActiveRecord - end - - class ExternalIssue < Grape::Entity - expose :title - expose :id - end - - class PipelineBasic < Grape::Entity - expose :id, :sha, :ref, :status - expose :created_at, :updated_at - - expose :web_url do |pipeline, _options| - Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline) - end - end - - class MergeRequestSimple < IssuableEntity - expose :title - expose :web_url do |merge_request, options| - Gitlab::UrlBuilder.build(merge_request) - end - end - - class MergeRequestBasic < IssuableEntity - expose :merged_by, using: Entities::UserBasic do |merge_request, _options| - merge_request.metrics&.merged_by - end - - expose :merged_at do |merge_request, _options| - merge_request.metrics&.merged_at - end - - expose :closed_by, using: Entities::UserBasic do |merge_request, _options| - merge_request.metrics&.latest_closed_by - end - - expose :closed_at do |merge_request, _options| - merge_request.metrics&.latest_closed_at - end - - expose :title_html, if: -> (_, options) { options[:render_html] } do |entity| - MarkupHelper.markdown_field(entity, :title) - end - expose :description_html, if: -> (_, options) { options[:render_html] } do |entity| - MarkupHelper.markdown_field(entity, :description) - end - expose :target_branch, :source_branch - expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) } - expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) } - expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) } - expose :assignee, using: ::API::Entities::UserBasic do |merge_request| - merge_request.assignee - end - expose :author, :assignees, using: Entities::UserBasic - - expose :source_project_id, :target_project_id - expose :labels do |merge_request, options| - if options[:with_labels_details] - ::API::Entities::LabelBasic.represent(merge_request.labels.sort_by(&:title)) - else - merge_request.labels.map(&:title).sort - end - end - expose :work_in_progress?, as: :work_in_progress - expose :milestone, using: Entities::Milestone - expose :merge_when_pipeline_succeeds - - # Ideally we should deprecate `MergeRequest#merge_status` exposure and - # use `MergeRequest#mergeable?` instead (boolean). - # See https://gitlab.com/gitlab-org/gitlab-foss/issues/42344 for more - # information. - expose :merge_status do |merge_request| - merge_request.check_mergeability - merge_request.merge_status - end - expose :diff_head_sha, as: :sha - expose :merge_commit_sha - expose :squash_commit_sha - expose :discussion_locked - expose :should_remove_source_branch?, as: :should_remove_source_branch - expose :force_remove_source_branch?, as: :force_remove_source_branch - expose :allow_collaboration, if: -> (merge_request, _) { merge_request.for_fork? } - # Deprecated - expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? } - - # reference is deprecated in favour of references - # Introduced [Gitlab 12.6](https://gitlab.com/gitlab-org/gitlab/merge_requests/20354) - expose :reference do |merge_request, options| - merge_request.to_reference(options[:project]) - end - - expose :references, with: IssuableReferences do |merge_request| - merge_request - end - - expose :web_url do |merge_request| - Gitlab::UrlBuilder.build(merge_request) - end - - expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |merge_request| - merge_request - end - - expose :squash - - expose :task_completion_status - - expose :cannot_be_merged?, as: :has_conflicts - - expose :mergeable_discussions_state?, as: :blocking_discussions_resolved - end - - class MergeRequest < MergeRequestBasic - expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |merge_request, options| - merge_request.subscribed?(options[:current_user], options[:project]) - end - - expose :changes_count do |merge_request, _options| - merge_request.merge_request_diff.real_size - end - - expose :latest_build_started_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options| - merge_request.metrics&.latest_build_started_at - end - - expose :latest_build_finished_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options| - merge_request.metrics&.latest_build_finished_at - end - - expose :first_deployed_to_production_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options| - merge_request.metrics&.first_deployed_to_production_at - end - - expose :pipeline, using: Entities::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options| - merge_request.metrics&.pipeline - end - - expose :head_pipeline, using: 'API::Entities::Pipeline', if: -> (_, options) do - Ability.allowed?(options[:current_user], :read_pipeline, options[:project]) - end - - expose :diff_refs, using: Entities::DiffRefs - - # Allow the status of a rebase to be determined - expose :merge_error - expose :rebase_in_progress?, as: :rebase_in_progress, if: -> (_, options) { options[:include_rebase_in_progress] } - - expose :diverged_commits_count, as: :diverged_commits_count, if: -> (_, options) { options[:include_diverged_commits_count] } - - def build_available?(options) - options[:project]&.feature_available?(:builds, options[:current_user]) - end - - expose :user do - expose :can_merge do |merge_request, options| - merge_request.can_be_merged_by?(options[:current_user]) - end - end - end - - class MergeRequestChanges < MergeRequest - expose :diffs, as: :changes, using: Entities::Diff do |compare, _| - compare.raw_diffs(limits: false).to_a - end - end - - class MergeRequestDiff < Grape::Entity - expose :id, :head_commit_sha, :base_commit_sha, :start_commit_sha, - :created_at, :merge_request_id, :state, :real_size - end - - class MergeRequestDiffFull < MergeRequestDiff - expose :commits, using: Entities::Commit - - expose :diffs, using: Entities::Diff do |compare, _| - compare.raw_diffs(limits: false).to_a - end - end - - class SSHKey < Grape::Entity - expose :id, :title, :key, :created_at - end - - class SSHKeyWithUser < SSHKey - expose :user, using: Entities::UserPublic - end - - class DeployKeyWithUser < SSHKeyWithUser - expose :deploy_keys_projects - end - - class DeployKeysProject < Grape::Entity - expose :deploy_key, merge: true, using: Entities::SSHKey - expose :can_push - end - - class GPGKey < Grape::Entity - expose :id, :key, :created_at - end - - class DiffPosition < Grape::Entity - expose :base_sha, :start_sha, :head_sha, :old_path, :new_path, - :position_type - end - - class Note < Grape::Entity - # Only Issue and MergeRequest have iid - NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze - - expose :id - expose :type - expose :note, as: :body - expose :attachment_identifier, as: :attachment - expose :author, using: Entities::UserBasic - expose :created_at, :updated_at - expose :system?, as: :system - expose :noteable_id, :noteable_type - - expose :position, if: ->(note, options) { note.is_a?(DiffNote) } do |note| - note.position.to_h - end - - expose :resolvable?, as: :resolvable - expose :resolved?, as: :resolved, if: ->(note, options) { note.resolvable? } - expose :resolved_by, using: Entities::UserBasic, if: ->(note, options) { note.resolvable? } - - # Avoid N+1 queries as much as possible - expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) } - end - - class Discussion < Grape::Entity - expose :id - expose :individual_note?, as: :individual_note - expose :notes, using: Entities::Note - end - - class Avatar < Grape::Entity - expose :avatar_url do |avatarable, options| - avatarable.avatar_url(only_path: false, size: options[:size]) - end - end - - class AwardEmoji < Grape::Entity - expose :id - expose :name - expose :user, using: Entities::UserBasic - expose :created_at, :updated_at - expose :awardable_id, :awardable_type - end - - class MRNote < Grape::Entity - expose :note - expose :author, using: Entities::UserBasic - end - - class CommitNote < Grape::Entity - expose :note - expose(:path) { |note| note.diff_file.try(:file_path) if note.diff_note? } - expose(:line) { |note| note.diff_line.try(:new_line) if note.diff_note? } - expose(:line_type) { |note| note.diff_line.try(:type) if note.diff_note? } - expose :author, using: Entities::UserBasic - expose :created_at - end - - class CommitStatus < Grape::Entity - expose :id, :sha, :ref, :status, :name, :target_url, :description, - :created_at, :started_at, :finished_at, :allow_failure, :coverage - expose :author, using: Entities::UserBasic - end - - class PushEventPayload < Grape::Entity - expose :commit_count, :action, :ref_type, :commit_from, :commit_to, :ref, - :commit_title, :ref_count - end - - class Event < Grape::Entity - expose :project_id, :action_name - expose :target_id, :target_iid, :target_type, :author_id - expose :target_title - expose :created_at - expose :note, using: Entities::Note, if: ->(event, options) { event.note? } - expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author } - - expose :push_event_payload, - as: :push_data, - using: PushEventPayload, - if: -> (event, _) { event.push_action? } - - expose :author_username do |event, options| - event.author&.username - end - end - - class ProjectGroupLink < Grape::Entity - expose :id, :project_id, :group_id, :group_access, :expires_at - end - - class Todo < Grape::Entity - expose :id - expose :project, using: Entities::ProjectIdentity, if: -> (todo, _) { todo.project_id } - expose :group, using: 'API::Entities::NamespaceBasic', if: -> (todo, _) { todo.group_id } - expose :author, using: Entities::UserBasic - expose :action_name - expose :target_type - - expose :target do |todo, options| - todo_options = options.fetch(todo.target_type, {}) - todo_target_class(todo.target_type).represent(todo.target, todo_options) - end - - expose :target_url do |todo, options| - todo_target_url(todo) - end - - expose :body - expose :state - expose :created_at - - def todo_target_class(target_type) - # false as second argument prevents looking up in module hierarchy - # see also https://gitlab.com/gitlab-org/gitlab-foss/issues/59719 - ::API::Entities.const_get(target_type, false) - end - - def todo_target_url(todo) - target_type = todo.target_type.underscore - target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url" - - Gitlab::Routing - .url_helpers - .public_send(target_url, todo.resource_parent, todo.target, anchor: todo_target_anchor(todo)) # rubocop:disable GitlabSecurity/PublicSend - end - - def todo_target_anchor(todo) - "note_#{todo.note_id}" if todo.note_id? - end - end - - class NamespaceBasic < Grape::Entity - expose :id, :name, :path, :kind, :full_path, :parent_id, :avatar_url - - expose :web_url do |namespace| - if namespace.user? - Gitlab::Routing.url_helpers.user_url(namespace.owner) - else - namespace.web_url - end - end - end - - class Namespace < NamespaceBasic - expose :members_count_with_descendants, if: -> (namespace, opts) { expose_members_count_with_descendants?(namespace, opts) } do |namespace, _| - namespace.users_with_descendants.count - end - - def expose_members_count_with_descendants?(namespace, opts) - namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace) - end - end - - class MemberAccess < Grape::Entity - expose :access_level - expose :notification_level do |member, options| - if member.notification_setting - ::NotificationSetting.levels[member.notification_setting.level] - end - end - end - - class ProjectAccess < MemberAccess - end - - class GroupAccess < MemberAccess - end - - class NotificationSetting < Grape::Entity - expose :level - expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do - ::NotificationSetting.email_events.each do |event| - expose event - end - end - end - - class GlobalNotificationSetting < NotificationSetting - expose :notification_email do |notification_setting, options| - notification_setting.user.notification_email - end - end - - class ProjectServiceBasic < Grape::Entity - expose :id, :title - expose :slug do |service| - service.to_param.dasherize - end - expose :created_at, :updated_at, :active - expose :commit_events, :push_events, :issues_events, :confidential_issues_events - expose :merge_requests_events, :tag_push_events, :note_events - expose :confidential_note_events, :pipeline_events, :wiki_page_events - expose :job_events, :comment_on_event_enabled - end - - class ProjectService < ProjectServiceBasic - # Expose serialized properties - expose :properties do |service, options| - # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 - if service.data_fields_present? - service.data_fields.as_json.slice(*service.api_field_names) - else - service.properties.slice(*service.api_field_names) - end - end - end - - class ProjectWithAccess < Project - expose :permissions do - expose :project_access, using: Entities::ProjectAccess do |project, options| - if options[:project_members] - options[:project_members].find { |member| member.source_id == project.id } - else - project.project_member(options[:current_user]) - end - end - - expose :group_access, using: Entities::GroupAccess do |project, options| - if project.group - if options[:group_members] - options[:group_members].find { |member| member.source_id == project.namespace_id } - else - project.group.highest_group_member(options[:current_user]) - end - end - end - end - - # rubocop: disable CodeReuse/ActiveRecord - def self.preload_relation(projects_relation, options = {}) - relation = super(projects_relation, options) - project_ids = relation.select('projects.id') - namespace_ids = relation.select(:namespace_id) - - options[:project_members] = options[:current_user] - .project_members - .where(source_id: project_ids) - .preload(:source, user: [notification_settings: :source]) - - options[:group_members] = options[:current_user] - .group_members - .where(source_id: namespace_ids) - .preload(:source, user: [notification_settings: :source]) - - relation - end - # rubocop: enable CodeReuse/ActiveRecord - end - - class LabelBasic < Grape::Entity - expose :id, :name, :color, :description, :description_html, :text_color - end - - class Label < LabelBasic - with_options if: lambda { |_, options| options[:with_counts] } do - expose :open_issues_count do |label, options| - label.open_issues_count(options[:current_user]) - end - - expose :closed_issues_count do |label, options| - label.closed_issues_count(options[:current_user]) - end - - expose :open_merge_requests_count do |label, options| - label.open_merge_requests_count(options[:current_user]) - end - end - - expose :subscribed do |label, options| - label.subscribed?(options[:current_user], options[:parent]) - end - end - - class GroupLabel < Label - end - - class ProjectLabel < Label - expose :priority do |label, options| - label.priority(options[:parent]) - end - expose :is_project_label do |label, options| - label.is_a?(::ProjectLabel) - end - end - - class List < Grape::Entity - expose :id - expose :label, using: Entities::LabelBasic - expose :position - end - - class Board < Grape::Entity - expose :id - expose :project, using: Entities::BasicProjectDetails - - expose :lists, using: Entities::List do |board| - board.destroyable_lists - end - end - - class Compare < Grape::Entity - expose :commit, using: Entities::Commit do |compare, options| - ::Commit.decorate(compare.commits, nil).last - end - - expose :commits, using: Entities::Commit do |compare, options| - ::Commit.decorate(compare.commits, nil) - end - - expose :diffs, using: Entities::Diff do |compare, options| - compare.diffs(limits: false).to_a - end - - expose :compare_timeout do |compare, options| - compare.diffs.overflow? - end - - expose :same, as: :compare_same_ref - end - - class Contributor < Grape::Entity - expose :name, :email, :commits, :additions, :deletions - end - - class BroadcastMessage < Grape::Entity - expose :message, :starts_at, :ends_at, :color, :font, :target_path - end - - class ApplicationStatistics < Grape::Entity - include ActionView::Helpers::NumberHelper - include CountHelper - - expose :forks do |counts| - approximate_fork_count_with_delimiters(counts) - end - - expose :issues do |counts| - approximate_count_with_delimiters(counts, ::Issue) - end - - expose :merge_requests do |counts| - approximate_count_with_delimiters(counts, ::MergeRequest) - end - - expose :notes do |counts| - approximate_count_with_delimiters(counts, ::Note) - end - - expose :snippets do |counts| - approximate_count_with_delimiters(counts, ::Snippet) - end - - expose :ssh_keys do |counts| - approximate_count_with_delimiters(counts, ::Key) - end - - expose :milestones do |counts| - approximate_count_with_delimiters(counts, ::Milestone) - end - - expose :users do |counts| - approximate_count_with_delimiters(counts, ::User) - end - - expose :projects do |counts| - approximate_count_with_delimiters(counts, ::Project) - end - - expose :groups do |counts| - approximate_count_with_delimiters(counts, ::Group) - end - - expose :active_users do |_| - number_with_delimiter(::User.active.count) - end - end - - class ApplicationSetting < Grape::Entity - def self.exposed_attributes - attributes = ::ApplicationSettingsHelper.visible_attributes - attributes.delete(:performance_bar_allowed_group_path) - attributes.delete(:performance_bar_enabled) - attributes.delete(:allow_local_requests_from_hooks_and_services) - - # let's not expose the secret key in a response - attributes.delete(:asset_proxy_secret_key) - attributes.delete(:eks_secret_access_key) - - attributes - end - - expose :id, :performance_bar_allowed_group_id - expose(*exposed_attributes) - expose(:restricted_visibility_levels) do |setting, _options| - setting.restricted_visibility_levels.map { |level| Gitlab::VisibilityLevel.string_level(level) } - end - expose(:default_project_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_project_visibility) } - expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) } - expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) } - - expose(*::ApplicationSettingsHelper.external_authorization_service_attributes) - - # support legacy names, can be removed in v5 - expose :password_authentication_enabled_for_web, as: :password_authentication_enabled - expose :password_authentication_enabled_for_web, as: :signin_enabled - expose :allow_local_requests_from_web_hooks_and_services, as: :allow_local_requests_from_hooks_and_services - end - - class Appearance < Grape::Entity - expose :title - expose :description - - expose :logo do |appearance, options| - appearance.logo.url - end - - expose :header_logo do |appearance, options| - appearance.header_logo.url - end - - expose :favicon do |appearance, options| - appearance.favicon.url - end - - expose :new_project_guidelines - expose :header_message - expose :footer_message - expose :message_background_color - expose :message_font_color - expose :email_header_and_footer_enabled - end - - # deprecated old Release representation - class TagRelease < Grape::Entity - expose :tag, as: :tag_name - expose :description - end - - module Releases - class Link < Grape::Entity - expose :id - expose :name - expose :url - expose :external?, as: :external - end - - class Source < Grape::Entity - expose :format - expose :url - end - end - - class Release < Grape::Entity - include ::API::Helpers::Presentable - - expose :name do |release, _| - can_download_code? ? release.name : "Release-#{release.id}" - end - expose :tag, as: :tag_name, if: ->(_, _) { can_download_code? } - expose :description - expose :description_html do |entity| - MarkupHelper.markdown_field(entity, :description) - end - expose :created_at - expose :released_at - expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? } - expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? } - expose :upcoming_release?, as: :upcoming_release - expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? && can_read_milestone? } - expose :commit_path, expose_nil: false - expose :tag_path, expose_nil: false - expose :evidence_sha, expose_nil: false, if: ->(_, _) { can_download_code? } - expose :assets do - expose :assets_count, as: :count do |release, _| - assets_to_exclude = can_download_code? ? [] : [:sources] - release.assets_count(except: assets_to_exclude) - end - expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_download_code? } - expose :links, using: Entities::Releases::Link do |release, options| - release.links.sorted - end - expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? } - end - expose :_links do - expose :merge_requests_url, expose_nil: false - expose :issues_url, expose_nil: false - expose :edit_url, expose_nil: false - end - - private - - def can_download_code? - Ability.allowed?(options[:current_user], :download_code, object.project) - end - - def can_read_milestone? - Ability.allowed?(options[:current_user], :read_milestone, object.project) - end - end - - class Tag < Grape::Entity - expose :name, :message, :target - - expose :commit, using: Entities::Commit do |repo_tag, options| - options[:project].repository.commit(repo_tag.dereferenced_target) - end - - # rubocop: disable CodeReuse/ActiveRecord - expose :release, using: Entities::TagRelease do |repo_tag, options| - options[:project].releases.find_by(tag: repo_tag.name) - end - # rubocop: enable CodeReuse/ActiveRecord - - expose :protected do |repo_tag, options| - ::ProtectedTag.protected?(options[:project], repo_tag.name) - end - end - - class Runner < Grape::Entity - expose :id - expose :description - expose :ip_address - expose :active - expose :instance_type?, as: :is_shared - expose :name - expose :online?, as: :online - expose :status - end - - class RunnerDetails < Runner - expose :tag_list - expose :run_untagged - expose :locked - expose :maximum_timeout - expose :access_level - expose :version, :revision, :platform, :architecture - expose :contacted_at - expose :token, if: lambda { |runner, options| options[:current_user].admin? || !runner.instance_type? } - # rubocop: disable CodeReuse/ActiveRecord - expose :projects, with: Entities::BasicProjectDetails do |runner, options| - if options[:current_user].admin? - runner.projects - else - options[:current_user].authorized_projects.where(id: runner.projects) - end - end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord - expose :groups, with: Entities::BasicGroupDetails do |runner, options| - if options[:current_user].admin? - runner.groups - else - options[:current_user].authorized_groups.where(id: runner.groups) - end - end - # rubocop: enable CodeReuse/ActiveRecord - end - - class RunnerRegistrationDetails < Grape::Entity - expose :id, :token - end - - class JobArtifactFile < Grape::Entity - expose :filename - expose :cached_size, as: :size - end - - class JobArtifact < Grape::Entity - expose :file_type, :size, :filename, :file_format - end - - class JobBasic < Grape::Entity - expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure - expose :created_at, :started_at, :finished_at - expose :duration - expose :user, with: User - expose :commit, with: Commit - expose :pipeline, with: PipelineBasic - - expose :web_url do |job, _options| - Gitlab::Routing.url_helpers.project_job_url(job.project, job) - end - end - - class Job < JobBasic - # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5) - expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? } - expose :job_artifacts, as: :artifacts, using: JobArtifact - expose :runner, with: Runner - expose :artifacts_expire_at - end - - class JobBasicWithProject < JobBasic - expose :project, with: ProjectIdentity - end - - class Trigger < Grape::Entity - include ::API::Helpers::Presentable - - expose :id - expose :token - expose :description - expose :created_at, :updated_at, :last_used - expose :owner, using: Entities::UserBasic - end - - class Variable < Grape::Entity - expose :variable_type, :key, :value - expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) } - expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) } - expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) } - end - - class Pipeline < PipelineBasic - expose :before_sha, :tag, :yaml_errors - - expose :user, with: Entities::UserBasic - expose :created_at, :updated_at, :started_at, :finished_at, :committed_at - expose :duration - expose :coverage - expose :detailed_status, using: DetailedStatusEntity do |pipeline, options| - pipeline.detailed_status(options[:current_user]) - end - end - - class PipelineSchedule < Grape::Entity - expose :id - expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active - expose :created_at, :updated_at - expose :owner, using: Entities::UserBasic - end - - class PipelineScheduleDetails < PipelineSchedule - expose :last_pipeline, using: Entities::PipelineBasic - expose :variables, using: Entities::Variable - end - - class EnvironmentBasic < Grape::Entity - expose :id, :name, :slug, :external_url - end - - class Deployment < Grape::Entity - expose :id, :iid, :ref, :sha, :created_at, :updated_at - expose :user, using: Entities::UserBasic - expose :environment, using: Entities::EnvironmentBasic - expose :deployable, using: Entities::Job - expose :status - end - - class Environment < EnvironmentBasic - expose :project, using: Entities::BasicProjectDetails - expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true } - expose :state - end - - class LicenseBasic < Grape::Entity - expose :key, :name, :nickname - expose :url, as: :html_url - expose(:source_url) { |license| license.meta['source'] } - end - - class License < LicenseBasic - expose :popular?, as: :popular - expose(:description) { |license| license.meta['description'] } - expose(:conditions) { |license| license.meta['conditions'] } - expose(:permissions) { |license| license.meta['permissions'] } - expose(:limitations) { |license| license.meta['limitations'] } - expose :content - end - - class TemplatesList < Grape::Entity - expose :key - expose :name - end - - class Template < Grape::Entity - expose :name, :content - end - - class BroadcastMessage < Grape::Entity - expose :id, :message, :starts_at, :ends_at, :color, :font - expose :active?, as: :active - end - - class PersonalAccessToken < Grape::Entity - expose :id, :name, :revoked, :created_at, :scopes - expose :active?, as: :active - expose :expires_at do |personal_access_token| - personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil - end - end - - class PersonalAccessTokenWithToken < PersonalAccessToken - expose :token - end - - class ImpersonationToken < PersonalAccessToken - expose :impersonation - end - - class ImpersonationTokenWithToken < PersonalAccessTokenWithToken - expose :impersonation - end - - class FeatureGate < Grape::Entity - expose :key - expose :value - end - - class Feature < Grape::Entity - expose :name - expose :state - expose :gates, using: FeatureGate do |model| - model.gates.map do |gate| - value = model.gate_values[gate.key] - - # By default all gate values are populated. Only show relevant ones. - if (value.is_a?(Integer) && value.zero?) || (value.is_a?(Set) && value.empty?) - next - end - - { key: gate.key, value: value } - end.compact - end - end - - module JobRequest - class JobInfo < Grape::Entity - expose :name, :stage - expose :project_id, :project_name - end - - class GitInfo < Grape::Entity - expose :repo_url, :ref, :sha, :before_sha - expose :ref_type - expose :refspecs - expose :git_depth, as: :depth - end - - class RunnerInfo < Grape::Entity - expose :metadata_timeout, as: :timeout - expose :runner_session_url - end - - class Step < Grape::Entity - expose :name, :script, :timeout, :when, :allow_failure - end - - class Port < Grape::Entity - expose :number, :protocol, :name - end - - class Image < Grape::Entity - expose :name, :entrypoint - expose :ports, using: JobRequest::Port - end - - class Service < Image - expose :alias, :command - end - - class Artifacts < Grape::Entity - expose :name - expose :untracked - expose :paths - expose :when - expose :expire_in - expose :artifact_type - expose :artifact_format - end - - class Cache < Grape::Entity - expose :key, :untracked, :paths, :policy - end - - class Credentials < Grape::Entity - expose :type, :url, :username, :password - end - - class Dependency < Grape::Entity - expose :id, :name, :token - expose :artifacts_file, using: JobArtifactFile, if: ->(job, _) { job.artifacts? } - end - - class Response < Grape::Entity - expose :id - expose :token - expose :allow_git_fetch - - expose :job_info, using: JobInfo do |model| - model - end - - expose :git_info, using: GitInfo do |model| - model - end - - expose :runner_info, using: RunnerInfo do |model| - model - end - - expose :variables - expose :steps, using: Step - expose :image, using: Image - expose :services, using: Service - expose :artifacts, using: Artifacts - expose :cache, using: Cache - expose :credentials, using: Credentials - expose :all_dependencies, as: :dependencies, using: Dependency - expose :features - end - end - - class UserAgentDetail < Grape::Entity - expose :user_agent - expose :ip_address - expose :submitted, as: :akismet_submitted - end - - class CustomAttribute < Grape::Entity - expose :key - expose :value - end - - class PagesDomainCertificateExpiration < Grape::Entity - expose :expired?, as: :expired - expose :expiration - end - - class PagesDomainCertificate < Grape::Entity - expose :subject - expose :expired?, as: :expired - expose :certificate - expose :certificate_text - end - - class PagesDomainBasic < Grape::Entity - expose :domain - expose :url - expose :project_id - expose :verified?, as: :verified - expose :verification_code, as: :verification_code - expose :enabled_until - expose :auto_ssl_enabled - - expose :certificate, - as: :certificate_expiration, - if: ->(pages_domain, _) { pages_domain.certificate? }, - using: PagesDomainCertificateExpiration do |pages_domain| - pages_domain - end - end - - class PagesDomain < Grape::Entity - expose :domain - expose :url - expose :verified?, as: :verified - expose :verification_code, as: :verification_code - expose :enabled_until - expose :auto_ssl_enabled - - expose :certificate, - if: ->(pages_domain, _) { pages_domain.certificate? }, - using: PagesDomainCertificate do |pages_domain| - pages_domain - end - end - - class Application < Grape::Entity - expose :id - expose :uid, as: :application_id - expose :name, as: :application_name - expose :redirect_uri, as: :callback_url - end - - # Use with care, this exposes the secret - class ApplicationWithSecret < Application - expose :secret - end - - class Blob < Grape::Entity - expose :basename - expose :data - expose :path - # TODO: :filename was renamed to :path but both still return the full path, - # in the future we can only return the filename here without the leading - # directory path. - # https://gitlab.com/gitlab-org/gitlab/issues/34521 - expose :filename, &:path - expose :id - expose :ref - expose :startline - expose :project_id - end - - class BasicBadgeDetails < Grape::Entity - expose :name - expose :link_url - expose :image_url - expose :rendered_link_url do |badge, options| - badge.rendered_link_url(options.fetch(:project, nil)) - end - expose :rendered_image_url do |badge, options| - badge.rendered_image_url(options.fetch(:project, nil)) - end - end - - class Badge < BasicBadgeDetails - expose :id - expose :kind do |badge| - badge.type == 'ProjectBadge' ? 'project' : 'group' - end - end - - class ResourceLabelEvent < Grape::Entity - expose :id - expose :user, using: Entities::UserBasic - expose :created_at - expose :resource_type do |event, options| - event.issuable.class.name - end - expose :resource_id do |event, options| - event.issuable.id - end - expose :label, using: Entities::LabelBasic - expose :action - end - - class Suggestion < Grape::Entity - expose :id - expose :from_line - expose :to_line - expose :appliable?, as: :appliable - expose :applied - expose :from_content - expose :to_content - end - - module Platform - class Kubernetes < Grape::Entity - expose :api_url - expose :namespace - expose :authorization_type - expose :ca_cert - end - end - - module Provider - class Gcp < Grape::Entity - expose :cluster_id - expose :status_name - expose :gcp_project_id - expose :zone - expose :machine_type - expose :num_nodes - expose :endpoint - end - end - - class Cluster < Grape::Entity - expose :id, :name, :created_at, :domain - expose :provider_type, :platform_type, :environment_scope, :cluster_type - expose :user, using: Entities::UserBasic - expose :platform_kubernetes, using: Entities::Platform::Kubernetes - expose :provider_gcp, using: Entities::Provider::Gcp - expose :management_project, using: Entities::ProjectIdentity - end - - class ClusterProject < Cluster - expose :project, using: Entities::BasicProjectDetails - end - - class ClusterGroup < Cluster - expose :group, using: Entities::BasicGroupDetails - end - - module InternalPostReceive - class Message < Grape::Entity - expose :message - expose :type - end - - class Response < Grape::Entity - expose :messages, using: Message - expose :reference_counter_decreased - end - end - end -end - -# rubocop: disable Cop/InjectEnterpriseEditionModule -::API::Entities::ApplicationSetting.prepend_if_ee('EE::API::Entities::ApplicationSetting') -::API::Entities::Board.prepend_if_ee('EE::API::Entities::Board') -::API::Entities::Group.prepend_if_ee('EE::API::Entities::Group', with_descendants: true) -::API::Entities::GroupDetail.prepend_if_ee('EE::API::Entities::GroupDetail') -::API::Entities::IssueBasic.prepend_if_ee('EE::API::Entities::IssueBasic', with_descendants: true) -::API::Entities::Issue.prepend_if_ee('EE::API::Entities::Issue') -::API::Entities::List.prepend_if_ee('EE::API::Entities::List') -::API::Entities::MergeRequestBasic.prepend_if_ee('EE::API::Entities::MergeRequestBasic', with_descendants: true) -::API::Entities::Member.prepend_if_ee('EE::API::Entities::Member', with_descendants: true) -::API::Entities::Namespace.prepend_if_ee('EE::API::Entities::Namespace') -::API::Entities::Project.prepend_if_ee('EE::API::Entities::Project', with_descendants: true) -::API::Entities::ProtectedRefAccess.prepend_if_ee('EE::API::Entities::ProtectedRefAccess') -::API::Entities::UserPublic.prepend_if_ee('EE::API::Entities::UserPublic', with_descendants: true) -::API::Entities::Todo.prepend_if_ee('EE::API::Entities::Todo') -::API::Entities::ProtectedBranch.prepend_if_ee('EE::API::Entities::ProtectedBranch') -::API::Entities::Identity.prepend_if_ee('EE::API::Entities::Identity') -::API::Entities::UserWithAdmin.prepend_if_ee('EE::API::Entities::UserWithAdmin', with_descendants: true) diff --git a/lib/api/entities/access_requester.rb b/lib/api/entities/access_requester.rb new file mode 100644 index 00000000000..951250225cc --- /dev/null +++ b/lib/api/entities/access_requester.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class AccessRequester < Grape::Entity + expose :user, merge: true, using: UserBasic + expose :requested_at + end + end +end diff --git a/lib/api/entities/appearance.rb b/lib/api/entities/appearance.rb new file mode 100644 index 00000000000..c3cffc8d05c --- /dev/null +++ b/lib/api/entities/appearance.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module API + module Entities + class Appearance < Grape::Entity + expose :title + expose :description + + expose :logo do |appearance, options| + appearance.logo.url + end + + expose :header_logo do |appearance, options| + appearance.header_logo.url + end + + expose :favicon do |appearance, options| + appearance.favicon.url + end + + expose :new_project_guidelines + expose :header_message + expose :footer_message + expose :message_background_color + expose :message_font_color + expose :email_header_and_footer_enabled + end + end +end diff --git a/lib/api/entities/application.rb b/lib/api/entities/application.rb new file mode 100644 index 00000000000..33514200424 --- /dev/null +++ b/lib/api/entities/application.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class Application < Grape::Entity + expose :id + expose :uid, as: :application_id + expose :name, as: :application_name + expose :redirect_uri, as: :callback_url + expose :confidential + end + end +end diff --git a/lib/api/entities/application_setting.rb b/lib/api/entities/application_setting.rb new file mode 100644 index 00000000000..e9572a8d430 --- /dev/null +++ b/lib/api/entities/application_setting.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module API + module Entities + class ApplicationSetting < Grape::Entity + def self.exposed_attributes + attributes = ::ApplicationSettingsHelper.visible_attributes + attributes.delete(:performance_bar_allowed_group_path) + attributes.delete(:performance_bar_enabled) + attributes.delete(:allow_local_requests_from_hooks_and_services) + + # let's not expose the secret key in a response + attributes.delete(:asset_proxy_secret_key) + attributes.delete(:eks_secret_access_key) + + attributes + end + + expose :id, :performance_bar_allowed_group_id + expose(*exposed_attributes) + expose(:restricted_visibility_levels) do |setting, _options| + setting.restricted_visibility_levels.map { |level| Gitlab::VisibilityLevel.string_level(level) } + end + expose(:default_project_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_project_visibility) } + expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) } + expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) } + + expose(*::ApplicationSettingsHelper.external_authorization_service_attributes) + + # support legacy names, can be removed in v5 + expose :password_authentication_enabled_for_web, as: :password_authentication_enabled + expose :password_authentication_enabled_for_web, as: :signin_enabled + expose :allow_local_requests_from_web_hooks_and_services, as: :allow_local_requests_from_hooks_and_services + end + end +end + +API::Entities::ApplicationSetting.prepend_if_ee('EE::API::Entities::ApplicationSetting') diff --git a/lib/api/entities/application_statistics.rb b/lib/api/entities/application_statistics.rb new file mode 100644 index 00000000000..4bcba1da464 --- /dev/null +++ b/lib/api/entities/application_statistics.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module API + module Entities + class ApplicationStatistics < Grape::Entity + include ActionView::Helpers::NumberHelper + include CountHelper + + expose :forks do |counts| + approximate_fork_count_with_delimiters(counts) + end + + expose :issues do |counts| + approximate_count_with_delimiters(counts, ::Issue) + end + + expose :merge_requests do |counts| + approximate_count_with_delimiters(counts, ::MergeRequest) + end + + expose :notes do |counts| + approximate_count_with_delimiters(counts, ::Note) + end + + expose :snippets do |counts| + approximate_count_with_delimiters(counts, ::Snippet) + end + + expose :ssh_keys do |counts| + approximate_count_with_delimiters(counts, ::Key) + end + + expose :milestones do |counts| + approximate_count_with_delimiters(counts, ::Milestone) + end + + expose :users do |counts| + approximate_count_with_delimiters(counts, ::User) + end + + expose :projects do |counts| + approximate_count_with_delimiters(counts, ::Project) + end + + expose :groups do |counts| + approximate_count_with_delimiters(counts, ::Group) + end + + expose :active_users do |_| + number_with_delimiter(::User.active.count) + end + end + end +end diff --git a/lib/api/entities/application_with_secret.rb b/lib/api/entities/application_with_secret.rb new file mode 100644 index 00000000000..3e540381d89 --- /dev/null +++ b/lib/api/entities/application_with_secret.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + # Use with care, this exposes the secret + class ApplicationWithSecret < Entities::Application + expose :secret + end + end +end diff --git a/lib/api/entities/avatar.rb b/lib/api/entities/avatar.rb new file mode 100644 index 00000000000..7d5c762afcc --- /dev/null +++ b/lib/api/entities/avatar.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class Avatar < Grape::Entity + expose :avatar_url do |avatarable, options| + avatarable.avatar_url(only_path: false, size: options[:size]) + end + end + end +end diff --git a/lib/api/entities/award_emoji.rb b/lib/api/entities/award_emoji.rb new file mode 100644 index 00000000000..da9a183bf39 --- /dev/null +++ b/lib/api/entities/award_emoji.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class AwardEmoji < Grape::Entity + expose :id + expose :name + expose :user, using: Entities::UserBasic + expose :created_at, :updated_at + expose :awardable_id, :awardable_type + end + end +end diff --git a/lib/api/entities/badge.rb b/lib/api/entities/badge.rb new file mode 100644 index 00000000000..1e3e2ec469a --- /dev/null +++ b/lib/api/entities/badge.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class Badge < Entities::BasicBadgeDetails + expose :id + expose :kind do |badge| + badge.type == 'ProjectBadge' ? 'project' : 'group' + end + end + end +end diff --git a/lib/api/entities/basic_badge_details.rb b/lib/api/entities/basic_badge_details.rb new file mode 100644 index 00000000000..273dc57fe67 --- /dev/null +++ b/lib/api/entities/basic_badge_details.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + class BasicBadgeDetails < Grape::Entity + expose :name + expose :link_url + expose :image_url + expose :rendered_link_url do |badge, options| + badge.rendered_link_url(options.fetch(:project, nil)) + end + expose :rendered_image_url do |badge, options| + badge.rendered_image_url(options.fetch(:project, nil)) + end + end + end +end diff --git a/lib/api/entities/basic_group_details.rb b/lib/api/entities/basic_group_details.rb new file mode 100644 index 00000000000..882fce4ef2c --- /dev/null +++ b/lib/api/entities/basic_group_details.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class BasicGroupDetails < Grape::Entity + expose :id + expose :web_url + expose :name + end + end +end diff --git a/lib/api/entities/basic_project_details.rb b/lib/api/entities/basic_project_details.rb new file mode 100644 index 00000000000..13bc19456b3 --- /dev/null +++ b/lib/api/entities/basic_project_details.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module API + module Entities + class BasicProjectDetails < Entities::ProjectIdentity + include ::API::ProjectsRelationBuilder + + expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } + # Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770 + expose :tag_list do |project| + # project.tags.order(:name).pluck(:name) is the most suitable option + # to avoid loading all the ActiveRecord objects but, if we use it here + # it override the preloaded associations and makes a query + # (fixed in https://github.com/rails/rails/pull/25976). + project.tags.map(&:name).sort + end + + expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url + + expose :license_url, if: :license do |project| + license = project.repository.license_blob + + if license + Gitlab::Routing.url_helpers.project_blob_url(project, File.join(project.default_branch, license.path)) + end + end + + expose :license, with: 'API::Entities::LicenseBasic', if: :license do |project| + project.repository.license + end + + expose :avatar_url do |project, options| + project.avatar_url(only_path: false) + end + + expose :star_count, :forks_count + expose :last_activity_at + expose :namespace, using: 'API::Entities::NamespaceBasic' + expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes + + # rubocop: disable CodeReuse/ActiveRecord + def self.preload_relation(projects_relation, options = {}) + # Preloading tags, should be done with using only `:tags`, + # as `:tags` are defined as: `has_many :tags, through: :taggings` + # N+1 is solved then by using `subject.tags.map(&:name)` + # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/20555 + projects_relation.preload(:project_feature, :route) + .preload(:import_state, :tags) + .preload(:auto_devops) + .preload(namespace: [:route, :owner]) + end + # rubocop: enable CodeReuse/ActiveRecord + end + end +end diff --git a/lib/api/entities/basic_ref.rb b/lib/api/entities/basic_ref.rb new file mode 100644 index 00000000000..79c15075d99 --- /dev/null +++ b/lib/api/entities/basic_ref.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class BasicRef < Grape::Entity + expose :type, :name + end + end +end diff --git a/lib/api/entities/blame_range.rb b/lib/api/entities/blame_range.rb new file mode 100644 index 00000000000..20d09c15278 --- /dev/null +++ b/lib/api/entities/blame_range.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class BlameRange < Grape::Entity + expose :commit, using: BlameRangeCommit + expose :lines + end + end +end diff --git a/lib/api/entities/blame_range_commit.rb b/lib/api/entities/blame_range_commit.rb new file mode 100644 index 00000000000..3c1958925d7 --- /dev/null +++ b/lib/api/entities/blame_range_commit.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class BlameRangeCommit < Grape::Entity + expose :id + expose :parent_ids + expose :message + expose :authored_date, :author_name, :author_email + expose :committed_date, :committer_name, :committer_email + end + end +end diff --git a/lib/api/entities/blob.rb b/lib/api/entities/blob.rb new file mode 100644 index 00000000000..b14ef127b68 --- /dev/null +++ b/lib/api/entities/blob.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module API + module Entities + class Blob < Grape::Entity + expose :basename + expose :data + expose :path + # TODO: :filename was renamed to :path but both still return the full path, + # in the future we can only return the filename here without the leading + # directory path. + # https://gitlab.com/gitlab-org/gitlab/issues/34521 + expose :filename, &:path + expose :id + expose :ref + expose :startline + expose :project_id + end + end +end diff --git a/lib/api/entities/board.rb b/lib/api/entities/board.rb new file mode 100644 index 00000000000..5bb1cde0fa9 --- /dev/null +++ b/lib/api/entities/board.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + class Board < Grape::Entity + expose :id + expose :project, using: Entities::BasicProjectDetails + + expose :lists, using: Entities::List do |board| + board.destroyable_lists + end + end + end +end + +API::Entities::Board.prepend_if_ee('EE::API::Entities::Board') diff --git a/lib/api/entities/branch.rb b/lib/api/entities/branch.rb new file mode 100644 index 00000000000..1d5017ac702 --- /dev/null +++ b/lib/api/entities/branch.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module API + module Entities + class Branch < Grape::Entity + expose :name + + expose :commit, using: Entities::Commit do |repo_branch, options| + options[:project].repository.commit(repo_branch.dereferenced_target) + end + + expose :merged do |repo_branch, options| + if options[:merged_branch_names] + options[:merged_branch_names].include?(repo_branch.name) + else + options[:project].repository.merged_to_root_ref?(repo_branch) + end + end + + expose :protected do |repo_branch, options| + ::ProtectedBranch.protected?(options[:project], repo_branch.name) + end + + expose :developers_can_push do |repo_branch, options| + ::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches) + end + + expose :developers_can_merge do |repo_branch, options| + ::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches) + end + + expose :can_push do |repo_branch, options| + Gitlab::UserAccess.new(options[:current_user], project: options[:project]).can_push_to_branch?(repo_branch.name) + end + + expose :default do |repo_branch, options| + options[:project].default_branch == repo_branch.name + end + end + end +end diff --git a/lib/api/entities/broadcast_message.rb b/lib/api/entities/broadcast_message.rb new file mode 100644 index 00000000000..403677aa300 --- /dev/null +++ b/lib/api/entities/broadcast_message.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class BroadcastMessage < Grape::Entity + expose :id, :message, :starts_at, :ends_at, :color, :font, :target_path, :broadcast_type + expose :active?, as: :active + end + end +end diff --git a/lib/api/entities/cluster.rb b/lib/api/entities/cluster.rb new file mode 100644 index 00000000000..4cb54e988ce --- /dev/null +++ b/lib/api/entities/cluster.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class Cluster < Grape::Entity + expose :id, :name, :created_at, :domain + expose :provider_type, :platform_type, :environment_scope, :cluster_type + expose :user, using: Entities::UserBasic + expose :platform_kubernetes, using: Entities::Platform::Kubernetes + expose :provider_gcp, using: Entities::Provider::Gcp + expose :management_project, using: Entities::ProjectIdentity + end + end +end diff --git a/lib/api/entities/cluster_group.rb b/lib/api/entities/cluster_group.rb new file mode 100644 index 00000000000..8f71438cf3d --- /dev/null +++ b/lib/api/entities/cluster_group.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class ClusterGroup < Entities::Cluster + expose :group, using: Entities::BasicGroupDetails + end + end +end diff --git a/lib/api/entities/cluster_project.rb b/lib/api/entities/cluster_project.rb new file mode 100644 index 00000000000..2fd3e35e2a2 --- /dev/null +++ b/lib/api/entities/cluster_project.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class ClusterProject < Entities::Cluster + expose :project, using: Entities::BasicProjectDetails + end + end +end diff --git a/lib/api/entities/commit.rb b/lib/api/entities/commit.rb new file mode 100644 index 00000000000..7ce97c2c3d8 --- /dev/null +++ b/lib/api/entities/commit.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class Commit < Grape::Entity + expose :id, :short_id, :created_at + expose :parent_ids + expose :full_title, as: :title + expose :safe_message, as: :message + expose :author_name, :author_email, :authored_date + expose :committer_name, :committer_email, :committed_date + end + end +end diff --git a/lib/api/entities/commit_detail.rb b/lib/api/entities/commit_detail.rb new file mode 100644 index 00000000000..22424b38bb9 --- /dev/null +++ b/lib/api/entities/commit_detail.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module API + module Entities + class CommitDetail < Commit + expose :stats, using: Entities::CommitStats, if: :stats + expose :status + expose :project_id + + expose :last_pipeline do |commit, options| + pipeline = commit.last_pipeline if can_read_pipeline? + ::API::Entities::PipelineBasic.represent(pipeline, options) + end + + private + + def can_read_pipeline? + Ability.allowed?(options[:current_user], :read_pipeline, object.last_pipeline) + end + end + end +end diff --git a/lib/api/entities/commit_note.rb b/lib/api/entities/commit_note.rb new file mode 100644 index 00000000000..d08b6fc8225 --- /dev/null +++ b/lib/api/entities/commit_note.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class CommitNote < Grape::Entity + expose :note + expose(:path) { |note| note.diff_file.try(:file_path) if note.diff_note? } + expose(:line) { |note| note.diff_line.try(:new_line) if note.diff_note? } + expose(:line_type) { |note| note.diff_line.try(:type) if note.diff_note? } + expose :author, using: Entities::UserBasic + expose :created_at + end + end +end diff --git a/lib/api/entities/commit_signature.rb b/lib/api/entities/commit_signature.rb new file mode 100644 index 00000000000..8e86d4c1aa6 --- /dev/null +++ b/lib/api/entities/commit_signature.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class CommitSignature < Grape::Entity + expose :gpg_key_id + expose :gpg_key_primary_keyid, :gpg_key_user_name, :gpg_key_user_email + expose :verification_status + expose :gpg_key_subkey_id + end + end +end diff --git a/lib/api/entities/commit_stats.rb b/lib/api/entities/commit_stats.rb new file mode 100644 index 00000000000..d9ba99c8eb0 --- /dev/null +++ b/lib/api/entities/commit_stats.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class CommitStats < Grape::Entity + expose :additions, :deletions, :total + end + end +end diff --git a/lib/api/entities/commit_status.rb b/lib/api/entities/commit_status.rb new file mode 100644 index 00000000000..61b8bf89cfe --- /dev/null +++ b/lib/api/entities/commit_status.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class CommitStatus < Grape::Entity + expose :id, :sha, :ref, :status, :name, :target_url, :description, + :created_at, :started_at, :finished_at, :allow_failure, :coverage + expose :author, using: Entities::UserBasic + end + end +end diff --git a/lib/api/entities/commit_with_stats.rb b/lib/api/entities/commit_with_stats.rb new file mode 100644 index 00000000000..8a992586e22 --- /dev/null +++ b/lib/api/entities/commit_with_stats.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class CommitWithStats < Commit + expose :stats, using: Entities::CommitStats + end + end +end diff --git a/lib/api/entities/compare.rb b/lib/api/entities/compare.rb new file mode 100644 index 00000000000..fe2f03db2af --- /dev/null +++ b/lib/api/entities/compare.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module API + module Entities + class Compare < Grape::Entity + expose :commit, using: Entities::Commit do |compare, _| + compare.commits.last + end + + expose :commits, using: Entities::Commit do |compare, _| + compare.commits + end + + expose :diffs, using: Entities::Diff do |compare, _| + compare.diffs.diffs.to_a + end + + expose :compare_timeout do |compare, _| + compare.diffs.diffs.overflow? + end + + expose :same, as: :compare_same_ref + end + end +end diff --git a/lib/api/entities/container_expiration_policy.rb b/lib/api/entities/container_expiration_policy.rb new file mode 100644 index 00000000000..853bbb9b76b --- /dev/null +++ b/lib/api/entities/container_expiration_policy.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class ContainerExpirationPolicy < Grape::Entity + expose :cadence + expose :enabled + expose :keep_n + expose :older_than + expose :name_regex + expose :next_run_at + end + end +end diff --git a/lib/api/entities/contributor.rb b/lib/api/entities/contributor.rb new file mode 100644 index 00000000000..8763822b674 --- /dev/null +++ b/lib/api/entities/contributor.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class Contributor < Grape::Entity + expose :name, :email, :commits, :additions, :deletions + end + end +end diff --git a/lib/api/entities/custom_attribute.rb b/lib/api/entities/custom_attribute.rb new file mode 100644 index 00000000000..f949b709517 --- /dev/null +++ b/lib/api/entities/custom_attribute.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class CustomAttribute < Grape::Entity + expose :key + expose :value + end + end +end diff --git a/lib/api/entities/deploy_key_with_user.rb b/lib/api/entities/deploy_key_with_user.rb new file mode 100644 index 00000000000..31024dc3910 --- /dev/null +++ b/lib/api/entities/deploy_key_with_user.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class DeployKeyWithUser < Entities::SSHKeyWithUser + expose :deploy_keys_projects + end + end +end diff --git a/lib/api/entities/deploy_keys_project.rb b/lib/api/entities/deploy_keys_project.rb new file mode 100644 index 00000000000..64725459167 --- /dev/null +++ b/lib/api/entities/deploy_keys_project.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class DeployKeysProject < Grape::Entity + expose :deploy_key, merge: true, using: Entities::SSHKey + expose :can_push + end + end +end diff --git a/lib/api/entities/deployment.rb b/lib/api/entities/deployment.rb new file mode 100644 index 00000000000..3a97d3e3c09 --- /dev/null +++ b/lib/api/entities/deployment.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class Deployment < Grape::Entity + expose :id, :iid, :ref, :sha, :created_at, :updated_at + expose :user, using: Entities::UserBasic + expose :environment, using: Entities::EnvironmentBasic + expose :deployable, using: Entities::Job + expose :status + end + end +end diff --git a/lib/api/entities/diff.rb b/lib/api/entities/diff.rb new file mode 100644 index 00000000000..e92bc5d6b68 --- /dev/null +++ b/lib/api/entities/diff.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class Diff < Grape::Entity + expose :old_path, :new_path, :a_mode, :b_mode + expose :new_file?, as: :new_file + expose :renamed_file?, as: :renamed_file + expose :deleted_file?, as: :deleted_file + expose :json_safe_diff, as: :diff + end + end +end diff --git a/lib/api/entities/diff_position.rb b/lib/api/entities/diff_position.rb new file mode 100644 index 00000000000..10150d04ac8 --- /dev/null +++ b/lib/api/entities/diff_position.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class DiffPosition < Grape::Entity + expose :base_sha, :start_sha, :head_sha, :old_path, :new_path, + :position_type + end + end +end diff --git a/lib/api/entities/diff_refs.rb b/lib/api/entities/diff_refs.rb new file mode 100644 index 00000000000..8772fa2334f --- /dev/null +++ b/lib/api/entities/diff_refs.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class DiffRefs < Grape::Entity + expose :base_sha, :head_sha, :start_sha + end + end +end diff --git a/lib/api/entities/discussion.rb b/lib/api/entities/discussion.rb new file mode 100644 index 00000000000..dd1dd40da23 --- /dev/null +++ b/lib/api/entities/discussion.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class Discussion < Grape::Entity + expose :id + expose :individual_note?, as: :individual_note + expose :notes, using: Entities::Note + end + end +end diff --git a/lib/api/entities/email.rb b/lib/api/entities/email.rb new file mode 100644 index 00000000000..5ba425def3d --- /dev/null +++ b/lib/api/entities/email.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class Email < Grape::Entity + expose :id, :email + end + end +end diff --git a/lib/api/entities/environment.rb b/lib/api/entities/environment.rb new file mode 100644 index 00000000000..cb39ce1b13a --- /dev/null +++ b/lib/api/entities/environment.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class Environment < Entities::EnvironmentBasic + expose :project, using: Entities::BasicProjectDetails + expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true } + expose :state + end + end +end diff --git a/lib/api/entities/environment_basic.rb b/lib/api/entities/environment_basic.rb new file mode 100644 index 00000000000..061d4739874 --- /dev/null +++ b/lib/api/entities/environment_basic.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class EnvironmentBasic < Grape::Entity + expose :id, :name, :slug, :external_url + end + end +end diff --git a/lib/api/entities/event.rb b/lib/api/entities/event.rb new file mode 100644 index 00000000000..9c2d766b7f1 --- /dev/null +++ b/lib/api/entities/event.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module API + module Entities + class Event < Grape::Entity + expose :project_id, :action_name + expose :target_id, :target_iid, :target_type, :author_id + expose :target_title + expose :created_at + expose :note, using: Entities::Note, if: ->(event, options) { event.note? } + expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author } + + expose :push_event_payload, + as: :push_data, + using: Entities::PushEventPayload, + if: -> (event, _) { event.push_action? } + + expose :author_username do |event, options| + event.author&.username + end + end + end +end diff --git a/lib/api/entities/external_issue.rb b/lib/api/entities/external_issue.rb new file mode 100644 index 00000000000..8a201f70099 --- /dev/null +++ b/lib/api/entities/external_issue.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class ExternalIssue < Grape::Entity + expose :title + expose :id + end + end +end diff --git a/lib/api/entities/feature.rb b/lib/api/entities/feature.rb new file mode 100644 index 00000000000..3c9182340ea --- /dev/null +++ b/lib/api/entities/feature.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module API + module Entities + class Feature < Grape::Entity + expose :name + expose :state + expose :gates, using: Entities::FeatureGate do |model| + model.gates.map do |gate| + value = model.gate_values[gate.key] + + # By default all gate values are populated. Only show relevant ones. + if (value.is_a?(Integer) && value.zero?) || (value.is_a?(Set) && value.empty?) + next + end + + { key: gate.key, value: value } + end.compact + end + end + end +end diff --git a/lib/api/entities/feature_gate.rb b/lib/api/entities/feature_gate.rb new file mode 100644 index 00000000000..bea9c9474b3 --- /dev/null +++ b/lib/api/entities/feature_gate.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class FeatureGate < Grape::Entity + expose :key + expose :value + end + end +end diff --git a/lib/api/entities/global_notification_setting.rb b/lib/api/entities/global_notification_setting.rb new file mode 100644 index 00000000000..f3ca64347f0 --- /dev/null +++ b/lib/api/entities/global_notification_setting.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class GlobalNotificationSetting < Entities::NotificationSetting + expose :notification_email do |notification_setting, options| + notification_setting.user.notification_email + end + end + end +end diff --git a/lib/api/entities/gpg_key.rb b/lib/api/entities/gpg_key.rb new file mode 100644 index 00000000000..a97e704a5dd --- /dev/null +++ b/lib/api/entities/gpg_key.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class GPGKey < Grape::Entity + expose :id, :key, :created_at + end + end +end diff --git a/lib/api/entities/group.rb b/lib/api/entities/group.rb new file mode 100644 index 00000000000..ae5ee4784ed --- /dev/null +++ b/lib/api/entities/group.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module API + module Entities + class Group < BasicGroupDetails + expose :path, :description, :visibility + expose :share_with_group_lock + expose :require_two_factor_authentication + expose :two_factor_grace_period + expose :project_creation_level_str, as: :project_creation_level + expose :auto_devops_enabled + expose :subgroup_creation_level_str, as: :subgroup_creation_level + expose :emails_disabled + expose :mentions_disabled + expose :lfs_enabled?, as: :lfs_enabled + expose :avatar_url do |group, options| + group.avatar_url(only_path: false) + end + expose :request_access_enabled + expose :full_name, :full_path + expose :parent_id + + expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes + + expose :statistics, if: :statistics do + with_options format_with: -> (value) { value.to_i } do + expose :storage_size + expose :repository_size + expose :wiki_size + expose :lfs_objects_size + expose :build_artifacts_size, as: :job_artifacts_size + end + end + end + end +end + +API::Entities::Group.prepend_if_ee('EE::API::Entities::Group', with_descendants: true) diff --git a/lib/api/entities/group_access.rb b/lib/api/entities/group_access.rb new file mode 100644 index 00000000000..5e53e9645c2 --- /dev/null +++ b/lib/api/entities/group_access.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module API + module Entities + class GroupAccess < MemberAccess + end + end +end diff --git a/lib/api/entities/group_detail.rb b/lib/api/entities/group_detail.rb new file mode 100644 index 00000000000..e03047a6e75 --- /dev/null +++ b/lib/api/entities/group_detail.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module API + module Entities + class GroupDetail < Group + expose :runners_token, if: lambda { |group, options| options[:user_can_admin_group] } + expose :projects, using: Entities::Project do |group, options| + projects = GroupProjectsFinder.new( + group: group, + current_user: options[:current_user], + options: { only_owned: true, limit: projects_limit } + ).execute + + Entities::Project.prepare_relation(projects) + end + + expose :shared_projects, using: Entities::Project do |group, options| + projects = GroupProjectsFinder.new( + group: group, + current_user: options[:current_user], + options: { only_shared: true, limit: projects_limit } + ).execute + + Entities::Project.prepare_relation(projects) + end + + def projects_limit + if ::Feature.enabled?(:limit_projects_in_groups_api, default_enabled: true) + GroupProjectsFinder::DEFAULT_PROJECTS_LIMIT + else + nil + end + end + end + end +end + +API::Entities::GroupDetail.prepend_if_ee('EE::API::Entities::GroupDetail') diff --git a/lib/api/entities/group_label.rb b/lib/api/entities/group_label.rb new file mode 100644 index 00000000000..4e1b9226e6d --- /dev/null +++ b/lib/api/entities/group_label.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module API + module Entities + class GroupLabel < Entities::Label + end + end +end diff --git a/lib/api/entities/hook.rb b/lib/api/entities/hook.rb new file mode 100644 index 00000000000..ac813bcac3f --- /dev/null +++ b/lib/api/entities/hook.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class Hook < Grape::Entity + expose :id, :url, :created_at, :push_events, :tag_push_events, :merge_requests_events, :repository_update_events + expose :enable_ssl_verification + end + end +end diff --git a/lib/api/entities/identity.rb b/lib/api/entities/identity.rb new file mode 100644 index 00000000000..52045b6250a --- /dev/null +++ b/lib/api/entities/identity.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class Identity < Grape::Entity + expose :provider, :extern_uid + end + end +end + +API::Entities::Identity.prepend_if_ee('EE::API::Entities::Identity') diff --git a/lib/api/entities/impersonation_token.rb b/lib/api/entities/impersonation_token.rb new file mode 100644 index 00000000000..9ee8f8bf77b --- /dev/null +++ b/lib/api/entities/impersonation_token.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class ImpersonationToken < Entities::PersonalAccessToken + expose :impersonation + end + end +end diff --git a/lib/api/entities/impersonation_token_with_token.rb b/lib/api/entities/impersonation_token_with_token.rb new file mode 100644 index 00000000000..4904f107628 --- /dev/null +++ b/lib/api/entities/impersonation_token_with_token.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class ImpersonationTokenWithToken < Entities::PersonalAccessTokenWithToken + expose :impersonation + end + end +end diff --git a/lib/api/entities/internal_post_receive/message.rb b/lib/api/entities/internal_post_receive/message.rb new file mode 100644 index 00000000000..3cfefa84d9b --- /dev/null +++ b/lib/api/entities/internal_post_receive/message.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module InternalPostReceive + class Message < Grape::Entity + expose :message + expose :type + end + end + end +end diff --git a/lib/api/entities/internal_post_receive/response.rb b/lib/api/entities/internal_post_receive/response.rb new file mode 100644 index 00000000000..c33418ed658 --- /dev/null +++ b/lib/api/entities/internal_post_receive/response.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module InternalPostReceive + class Response < Grape::Entity + expose :messages, using: Entities::InternalPostReceive::Message + expose :reference_counter_decreased + end + end + end +end diff --git a/lib/api/entities/issuable_entity.rb b/lib/api/entities/issuable_entity.rb new file mode 100644 index 00000000000..5bee59de539 --- /dev/null +++ b/lib/api/entities/issuable_entity.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module API + module Entities + class IssuableEntity < Grape::Entity + expose :id, :iid + expose(:project_id) { |entity| entity&.project.try(:id) } + expose :title, :description + expose :state, :created_at, :updated_at + + # Avoids an N+1 query when metadata is included + def issuable_metadata(subject, options, method, args = nil) + cached_subject = options.dig(:issuable_metadata, subject.id) + (cached_subject || subject).public_send(method, *args) # rubocop: disable GitlabSecurity/PublicSend + end + end + end +end diff --git a/lib/api/entities/issuable_references.rb b/lib/api/entities/issuable_references.rb new file mode 100644 index 00000000000..1bf078847cf --- /dev/null +++ b/lib/api/entities/issuable_references.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Entities + class IssuableReferences < Grape::Entity + expose :short do |issuable| + issuable.to_reference + end + + expose :relative do |issuable, options| + issuable.to_reference(options[:group] || options[:project]) + end + + expose :full do |issuable| + issuable.to_reference(full: true) + end + end + end +end diff --git a/lib/api/entities/issuable_time_stats.rb b/lib/api/entities/issuable_time_stats.rb new file mode 100644 index 00000000000..7c3452a10a1 --- /dev/null +++ b/lib/api/entities/issuable_time_stats.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module API + module Entities + class IssuableTimeStats < Grape::Entity + format_with(:time_tracking_formatter) do |time_spent| + Gitlab::TimeTrackingFormatter.output(time_spent) + end + + expose :time_estimate + expose :total_time_spent + expose :human_time_estimate + + with_options(format_with: :time_tracking_formatter) do + expose :total_time_spent, as: :human_total_time_spent + end + + # rubocop: disable CodeReuse/ActiveRecord + def total_time_spent + # Avoids an N+1 query since timelogs are preloaded + object.timelogs.map(&:time_spent).sum + end + # rubocop: enable CodeReuse/ActiveRecord + end + end +end diff --git a/lib/api/entities/issue.rb b/lib/api/entities/issue.rb new file mode 100644 index 00000000000..5f2609cf68b --- /dev/null +++ b/lib/api/entities/issue.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module API + module Entities + class Issue < IssueBasic + include ::API::Helpers::RelatedResourcesHelpers + + expose(:has_tasks) do |issue, _| + !issue.task_list_items.empty? + end + + expose :task_status, if: -> (issue, _) do + !issue.task_list_items.empty? + end + + expose :_links do + expose :self do |issue| + expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid)) + end + + expose :notes do |issue| + expose_url(api_v4_projects_issues_notes_path(id: issue.project_id, noteable_id: issue.iid)) + end + + expose :award_emoji do |issue| + expose_url(api_v4_projects_issues_award_emoji_path(id: issue.project_id, issue_iid: issue.iid)) + end + + expose :project do |issue| + expose_url(api_v4_projects_path(id: issue.project_id)) + end + end + + expose :references, with: IssuableReferences do |issue| + issue + end + + # Calculating the value of subscribed field triggers Markdown + # processing. We can't do that for multiple issues / merge + # requests in a single API request. + expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |issue, options| + issue.subscribed?(options[:current_user], options[:project] || issue.project) + end + + expose :moved_to_id + end + end +end + +API::Entities::Issue.prepend_if_ee('EE::API::Entities::Issue') diff --git a/lib/api/entities/issue_basic.rb b/lib/api/entities/issue_basic.rb new file mode 100644 index 00000000000..af92f4124f1 --- /dev/null +++ b/lib/api/entities/issue_basic.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module API + module Entities + class IssueBasic < IssuableEntity + expose :closed_at + expose :closed_by, using: Entities::UserBasic + + expose :labels do |issue, options| + if options[:with_labels_details] + ::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title)) + else + issue.labels.map(&:title).sort + end + end + + expose :milestone, using: Entities::Milestone + expose :assignees, :author, using: Entities::UserBasic + + expose :assignee, using: ::API::Entities::UserBasic do |issue| + issue.assignees.first + end + + expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) } + expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count, options[:current_user]) } + expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) } + expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) } + expose :due_date + expose :confidential + expose :discussion_locked + + expose :web_url do |issue| + Gitlab::UrlBuilder.build(issue) + end + + expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |issue| + issue + end + + expose :task_completion_status + end + end +end + +API::Entities::IssueBasic.prepend_if_ee('EE::API::Entities::IssueBasic', with_descendants: true) diff --git a/lib/api/entities/job.rb b/lib/api/entities/job.rb new file mode 100644 index 00000000000..cbee8794007 --- /dev/null +++ b/lib/api/entities/job.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class Job < Entities::JobBasic + # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5) + expose :artifacts_file, using: Entities::JobArtifactFile, if: -> (job, opts) { job.artifacts? } + expose :job_artifacts, as: :artifacts, using: Entities::JobArtifact + expose :runner, with: Entities::Runner + expose :artifacts_expire_at + end + end +end diff --git a/lib/api/entities/job_artifact.rb b/lib/api/entities/job_artifact.rb new file mode 100644 index 00000000000..94dbdb38fee --- /dev/null +++ b/lib/api/entities/job_artifact.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class JobArtifact < Grape::Entity + expose :file_type, :size, :filename, :file_format + end + end +end diff --git a/lib/api/entities/job_artifact_file.rb b/lib/api/entities/job_artifact_file.rb new file mode 100644 index 00000000000..fa2851a7f0e --- /dev/null +++ b/lib/api/entities/job_artifact_file.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class JobArtifactFile < Grape::Entity + expose :filename + expose :cached_size, as: :size + end + end +end diff --git a/lib/api/entities/job_basic.rb b/lib/api/entities/job_basic.rb new file mode 100644 index 00000000000..a8541039934 --- /dev/null +++ b/lib/api/entities/job_basic.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module API + module Entities + class JobBasic < Grape::Entity + expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure + expose :created_at, :started_at, :finished_at + expose :duration + expose :user, with: Entities::User + expose :commit, with: Entities::Commit + expose :pipeline, with: Entities::PipelineBasic + + expose :web_url do |job, _options| + Gitlab::Routing.url_helpers.project_job_url(job.project, job) + end + end + end +end diff --git a/lib/api/entities/job_basic_with_project.rb b/lib/api/entities/job_basic_with_project.rb new file mode 100644 index 00000000000..09387e045ec --- /dev/null +++ b/lib/api/entities/job_basic_with_project.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class JobBasicWithProject < Entities::JobBasic + expose :project, with: Entities::ProjectIdentity + end + end +end diff --git a/lib/api/entities/job_request/artifacts.rb b/lib/api/entities/job_request/artifacts.rb new file mode 100644 index 00000000000..c6871fdd875 --- /dev/null +++ b/lib/api/entities/job_request/artifacts.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Artifacts < Grape::Entity + expose :name + expose :untracked + expose :paths + expose :when + expose :expire_in + expose :artifact_type + expose :artifact_format + end + end + end +end diff --git a/lib/api/entities/job_request/cache.rb b/lib/api/entities/job_request/cache.rb new file mode 100644 index 00000000000..a75affbaf84 --- /dev/null +++ b/lib/api/entities/job_request/cache.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Cache < Grape::Entity + expose :key, :untracked, :paths, :policy + end + end + end +end diff --git a/lib/api/entities/job_request/credentials.rb b/lib/api/entities/job_request/credentials.rb new file mode 100644 index 00000000000..cdac5566cbd --- /dev/null +++ b/lib/api/entities/job_request/credentials.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Credentials < Grape::Entity + expose :type, :url, :username, :password + end + end + end +end diff --git a/lib/api/entities/job_request/dependency.rb b/lib/api/entities/job_request/dependency.rb new file mode 100644 index 00000000000..64d779f6575 --- /dev/null +++ b/lib/api/entities/job_request/dependency.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Dependency < Grape::Entity + expose :id, :name, :token + expose :artifacts_file, using: Entities::JobArtifactFile, if: ->(job, _) { job.artifacts? } + end + end + end +end diff --git a/lib/api/entities/job_request/git_info.rb b/lib/api/entities/job_request/git_info.rb new file mode 100644 index 00000000000..e07099263b5 --- /dev/null +++ b/lib/api/entities/job_request/git_info.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class GitInfo < Grape::Entity + expose :repo_url, :ref, :sha, :before_sha + expose :ref_type + expose :refspecs + expose :git_depth, as: :depth + end + end + end +end diff --git a/lib/api/entities/job_request/image.rb b/lib/api/entities/job_request/image.rb new file mode 100644 index 00000000000..47f4542d2d5 --- /dev/null +++ b/lib/api/entities/job_request/image.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Image < Grape::Entity + expose :name, :entrypoint + expose :ports, using: Entities::JobRequest::Port + end + end + end +end diff --git a/lib/api/entities/job_request/job_info.rb b/lib/api/entities/job_request/job_info.rb new file mode 100644 index 00000000000..09c13aa8471 --- /dev/null +++ b/lib/api/entities/job_request/job_info.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class JobInfo < Grape::Entity + expose :name, :stage + expose :project_id, :project_name + end + end + end +end diff --git a/lib/api/entities/job_request/port.rb b/lib/api/entities/job_request/port.rb new file mode 100644 index 00000000000..ee427da8657 --- /dev/null +++ b/lib/api/entities/job_request/port.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Port < Grape::Entity + expose :number, :protocol, :name + end + end + end +end diff --git a/lib/api/entities/job_request/response.rb b/lib/api/entities/job_request/response.rb new file mode 100644 index 00000000000..fdacd3af2da --- /dev/null +++ b/lib/api/entities/job_request/response.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Response < Grape::Entity + expose :id + expose :token + expose :allow_git_fetch + + expose :job_info, using: Entities::JobRequest::JobInfo do |model| + model + end + + expose :git_info, using: Entities::JobRequest::GitInfo do |model| + model + end + + expose :runner_info, using: Entities::JobRequest::RunnerInfo do |model| + model + end + + expose :variables + expose :steps, using: Entities::JobRequest::Step + expose :image, using: Entities::JobRequest::Image + expose :services, using: Entities::JobRequest::Service + expose :artifacts, using: Entities::JobRequest::Artifacts + expose :cache, using: Entities::JobRequest::Cache + expose :credentials, using: Entities::JobRequest::Credentials + expose :all_dependencies, as: :dependencies, using: Entities::JobRequest::Dependency + expose :features + end + end + end +end diff --git a/lib/api/entities/job_request/runner_info.rb b/lib/api/entities/job_request/runner_info.rb new file mode 100644 index 00000000000..e6d2e8d9e85 --- /dev/null +++ b/lib/api/entities/job_request/runner_info.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class RunnerInfo < Grape::Entity + expose :metadata_timeout, as: :timeout + expose :runner_session_url + end + end + end +end diff --git a/lib/api/entities/job_request/service.rb b/lib/api/entities/job_request/service.rb new file mode 100644 index 00000000000..9ad5abf4e9e --- /dev/null +++ b/lib/api/entities/job_request/service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Service < Entities::JobRequest::Image + expose :alias, :command + end + end + end +end diff --git a/lib/api/entities/job_request/step.rb b/lib/api/entities/job_request/step.rb new file mode 100644 index 00000000000..498dd017fb4 --- /dev/null +++ b/lib/api/entities/job_request/step.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module JobRequest + class Step < Grape::Entity + expose :name, :script, :timeout, :when, :allow_failure + end + end + end +end diff --git a/lib/api/entities/label.rb b/lib/api/entities/label.rb new file mode 100644 index 00000000000..ca9a0912331 --- /dev/null +++ b/lib/api/entities/label.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module API + module Entities + class Label < Entities::LabelBasic + with_options if: lambda { |_, options| options[:with_counts] } do + expose :open_issues_count do |label, options| + label.open_issues_count(options[:current_user]) + end + + expose :closed_issues_count do |label, options| + label.closed_issues_count(options[:current_user]) + end + + expose :open_merge_requests_count do |label, options| + label.open_merge_requests_count(options[:current_user]) + end + end + + expose :subscribed do |label, options| + label.subscribed?(options[:current_user], options[:parent]) + end + end + end +end diff --git a/lib/api/entities/label_basic.rb b/lib/api/entities/label_basic.rb new file mode 100644 index 00000000000..ed52688638e --- /dev/null +++ b/lib/api/entities/label_basic.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class LabelBasic < Grape::Entity + expose :id, :name, :color, :description, :description_html, :text_color + end + end +end diff --git a/lib/api/entities/license.rb b/lib/api/entities/license.rb new file mode 100644 index 00000000000..d7a414344c1 --- /dev/null +++ b/lib/api/entities/license.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class License < Entities::LicenseBasic + expose :popular?, as: :popular + expose(:description) { |license| license.meta['description'] } + expose(:conditions) { |license| license.meta['conditions'] } + expose(:permissions) { |license| license.meta['permissions'] } + expose(:limitations) { |license| license.meta['limitations'] } + expose :content + end + end +end diff --git a/lib/api/entities/license_basic.rb b/lib/api/entities/license_basic.rb new file mode 100644 index 00000000000..08af68785a9 --- /dev/null +++ b/lib/api/entities/license_basic.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class LicenseBasic < Grape::Entity + expose :key, :name, :nickname + expose :url, as: :html_url + expose(:source_url) { |license| license.meta['source'] } + end + end +end diff --git a/lib/api/entities/list.rb b/lib/api/entities/list.rb new file mode 100644 index 00000000000..480e722c22c --- /dev/null +++ b/lib/api/entities/list.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class List < Grape::Entity + expose :id + expose :label, using: Entities::LabelBasic + expose :position + end + end +end + +API::Entities::List.prepend_if_ee('EE::API::Entities::List') diff --git a/lib/api/entities/member.rb b/lib/api/entities/member.rb new file mode 100644 index 00000000000..14e97f41e77 --- /dev/null +++ b/lib/api/entities/member.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class Member < Grape::Entity + expose :user, merge: true, using: UserBasic + expose :access_level + expose :expires_at + end + end +end + +API::Entities::Member.prepend_if_ee('EE::API::Entities::Member', with_descendants: true) diff --git a/lib/api/entities/member_access.rb b/lib/api/entities/member_access.rb new file mode 100644 index 00000000000..097c72bf617 --- /dev/null +++ b/lib/api/entities/member_access.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class MemberAccess < Grape::Entity + expose :access_level + expose :notification_level do |member, options| + if member.notification_setting + ::NotificationSetting.levels[member.notification_setting.level] + end + end + end + end +end diff --git a/lib/api/entities/membership.rb b/lib/api/entities/membership.rb new file mode 100644 index 00000000000..2e3e6a0d8ba --- /dev/null +++ b/lib/api/entities/membership.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class Membership < Grape::Entity + expose :source_id + expose :source_name do |member| + member.source.name + end + expose :source_type + expose :access_level + end + end +end diff --git a/lib/api/entities/merge_request.rb b/lib/api/entities/merge_request.rb new file mode 100644 index 00000000000..9ff8e20ced1 --- /dev/null +++ b/lib/api/entities/merge_request.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module API + module Entities + class MergeRequest < MergeRequestBasic + expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |merge_request, options| + merge_request.subscribed?(options[:current_user], options[:project]) + end + + expose :changes_count do |merge_request, _options| + merge_request.merge_request_diff.real_size + end + + expose :latest_build_started_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options| + merge_request.metrics&.latest_build_started_at + end + + expose :latest_build_finished_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options| + merge_request.metrics&.latest_build_finished_at + end + + expose :first_deployed_to_production_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options| + merge_request.metrics&.first_deployed_to_production_at + end + + expose :pipeline, using: Entities::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options| + merge_request.metrics&.pipeline + end + + expose :head_pipeline, using: 'API::Entities::Pipeline', if: -> (_, options) do + Ability.allowed?(options[:current_user], :read_pipeline, options[:project]) + end + + expose :diff_refs, using: Entities::DiffRefs + + # Allow the status of a rebase to be determined + expose :merge_error + expose :rebase_in_progress?, as: :rebase_in_progress, if: -> (_, options) { options[:include_rebase_in_progress] } + + expose :diverged_commits_count, as: :diverged_commits_count, if: -> (_, options) { options[:include_diverged_commits_count] } + + def build_available?(options) + options[:project]&.feature_available?(:builds, options[:current_user]) + end + + expose :user do + expose :can_merge do |merge_request, options| + merge_request.can_be_merged_by?(options[:current_user]) + end + end + end + end +end diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb new file mode 100644 index 00000000000..8cec2c1a97e --- /dev/null +++ b/lib/api/entities/merge_request_basic.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +module API + module Entities + class MergeRequestBasic < IssuableEntity + expose :merged_by, using: Entities::UserBasic do |merge_request, _options| + merge_request.metrics&.merged_by + end + + expose :merged_at do |merge_request, _options| + merge_request.metrics&.merged_at + end + + expose :closed_by, using: Entities::UserBasic do |merge_request, _options| + merge_request.metrics&.latest_closed_by + end + + expose :closed_at do |merge_request, _options| + merge_request.metrics&.latest_closed_at + end + + expose :title_html, if: -> (_, options) { options[:render_html] } do |entity| + MarkupHelper.markdown_field(entity, :title) + end + expose :description_html, if: -> (_, options) { options[:render_html] } do |entity| + MarkupHelper.markdown_field(entity, :description) + end + expose :target_branch, :source_branch + expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) } + expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) } + expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) } + expose :assignee, using: ::API::Entities::UserBasic do |merge_request| + merge_request.assignee + end + expose :author, :assignees, using: Entities::UserBasic + + expose :source_project_id, :target_project_id + expose :labels do |merge_request, options| + if options[:with_labels_details] + ::API::Entities::LabelBasic.represent(merge_request.labels.sort_by(&:title)) + else + merge_request.labels.map(&:title).sort + end + end + expose :work_in_progress?, as: :work_in_progress + expose :milestone, using: Entities::Milestone + expose :merge_when_pipeline_succeeds + + # Ideally we should deprecate `MergeRequest#merge_status` exposure and + # use `MergeRequest#mergeable?` instead (boolean). + # See https://gitlab.com/gitlab-org/gitlab-foss/issues/42344 for more + # information. + expose :merge_status do |merge_request| + merge_request.check_mergeability(async: true) + merge_request.merge_status + end + expose :diff_head_sha, as: :sha + expose :merge_commit_sha + expose :squash_commit_sha + expose :discussion_locked + expose :should_remove_source_branch?, as: :should_remove_source_branch + expose :force_remove_source_branch?, as: :force_remove_source_branch + expose :allow_collaboration, if: -> (merge_request, _) { merge_request.for_fork? } + # Deprecated + expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? } + + # reference is deprecated in favour of references + # Introduced [Gitlab 12.6](https://gitlab.com/gitlab-org/gitlab/merge_requests/20354) + expose :reference do |merge_request, options| + merge_request.to_reference(options[:project]) + end + + expose :references, with: IssuableReferences do |merge_request| + merge_request + end + + expose :web_url do |merge_request| + Gitlab::UrlBuilder.build(merge_request) + end + + expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |merge_request| + merge_request + end + + expose :squash + + expose :task_completion_status + + expose :cannot_be_merged?, as: :has_conflicts + + expose :mergeable_discussions_state?, as: :blocking_discussions_resolved + end + end +end + +API::Entities::MergeRequestBasic.prepend_if_ee('EE::API::Entities::MergeRequestBasic', with_descendants: true) diff --git a/lib/api/entities/merge_request_changes.rb b/lib/api/entities/merge_request_changes.rb new file mode 100644 index 00000000000..a835d119736 --- /dev/null +++ b/lib/api/entities/merge_request_changes.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class MergeRequestChanges < MergeRequest + expose :diffs, as: :changes, using: Entities::Diff do |compare, _| + compare.raw_diffs(limits: false).to_a + end + end + end +end diff --git a/lib/api/entities/merge_request_diff.rb b/lib/api/entities/merge_request_diff.rb new file mode 100644 index 00000000000..3eda1400855 --- /dev/null +++ b/lib/api/entities/merge_request_diff.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class MergeRequestDiff < Grape::Entity + expose :id, :head_commit_sha, :base_commit_sha, :start_commit_sha, + :created_at, :merge_request_id, :state, :real_size + end + end +end diff --git a/lib/api/entities/merge_request_diff_full.rb b/lib/api/entities/merge_request_diff_full.rb new file mode 100644 index 00000000000..772b9b6822c --- /dev/null +++ b/lib/api/entities/merge_request_diff_full.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class MergeRequestDiffFull < MergeRequestDiff + expose :commits, using: Entities::Commit + + expose :diffs, using: Entities::Diff do |compare, _| + compare.raw_diffs(limits: false).to_a + end + end + end +end diff --git a/lib/api/entities/merge_request_simple.rb b/lib/api/entities/merge_request_simple.rb new file mode 100644 index 00000000000..f3ff4cc18a8 --- /dev/null +++ b/lib/api/entities/merge_request_simple.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class MergeRequestSimple < IssuableEntity + expose :title + expose :web_url do |merge_request, options| + Gitlab::UrlBuilder.build(merge_request) + end + end + end +end diff --git a/lib/api/entities/milestone.rb b/lib/api/entities/milestone.rb new file mode 100644 index 00000000000..5a0c222d691 --- /dev/null +++ b/lib/api/entities/milestone.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Entities + class Milestone < Grape::Entity + expose :id, :iid + expose :project_id, if: -> (entity, options) { entity&.project_id } + expose :group_id, if: -> (entity, options) { entity&.group_id } + expose :title, :description + expose :state, :created_at, :updated_at + expose :due_date + expose :start_date + + expose :web_url do |milestone, _options| + Gitlab::UrlBuilder.build(milestone) + end + end + end +end diff --git a/lib/api/entities/mr_note.rb b/lib/api/entities/mr_note.rb new file mode 100644 index 00000000000..283f7bd1092 --- /dev/null +++ b/lib/api/entities/mr_note.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class MRNote < Grape::Entity + expose :note + expose :author, using: Entities::UserBasic + end + end +end diff --git a/lib/api/entities/namespace.rb b/lib/api/entities/namespace.rb new file mode 100644 index 00000000000..a7e06cc3e02 --- /dev/null +++ b/lib/api/entities/namespace.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + class Namespace < Entities::NamespaceBasic + expose :members_count_with_descendants, if: -> (namespace, opts) { expose_members_count_with_descendants?(namespace, opts) } do |namespace, _| + namespace.users_with_descendants.count + end + + def expose_members_count_with_descendants?(namespace, opts) + namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace) + end + end + end +end + +API::Entities::Namespace.prepend_if_ee('EE::API::Entities::Namespace') diff --git a/lib/api/entities/namespace_basic.rb b/lib/api/entities/namespace_basic.rb new file mode 100644 index 00000000000..f968a074bd2 --- /dev/null +++ b/lib/api/entities/namespace_basic.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + class NamespaceBasic < Grape::Entity + expose :id, :name, :path, :kind, :full_path, :parent_id, :avatar_url + + expose :web_url do |namespace| + if namespace.user? + Gitlab::Routing.url_helpers.user_url(namespace.owner) + else + namespace.web_url + end + end + end + end +end diff --git a/lib/api/entities/note.rb b/lib/api/entities/note.rb new file mode 100644 index 00000000000..dcfb9a6d670 --- /dev/null +++ b/lib/api/entities/note.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module API + module Entities + class Note < Grape::Entity + # Only Issue and MergeRequest have iid + NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze + + expose :id + expose :type + expose :note, as: :body + expose :attachment_identifier, as: :attachment + expose :author, using: Entities::UserBasic + expose :created_at, :updated_at + expose :system?, as: :system + expose :noteable_id, :noteable_type + + expose :position, if: ->(note, options) { note.is_a?(DiffNote) } do |note| + note.position.to_h + end + + expose :resolvable?, as: :resolvable + expose :resolved?, as: :resolved, if: ->(note, options) { note.resolvable? } + expose :resolved_by, using: Entities::UserBasic, if: ->(note, options) { note.resolvable? } + + # Avoid N+1 queries as much as possible + expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) } + end + end +end diff --git a/lib/api/entities/notification_setting.rb b/lib/api/entities/notification_setting.rb new file mode 100644 index 00000000000..cdff4f2f5c5 --- /dev/null +++ b/lib/api/entities/notification_setting.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class NotificationSetting < Grape::Entity + expose :level + expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do + ::NotificationSetting.email_events.each do |event| + expose event + end + end + end + end +end diff --git a/lib/api/entities/pages_domain.rb b/lib/api/entities/pages_domain.rb new file mode 100644 index 00000000000..87af8c7b0a4 --- /dev/null +++ b/lib/api/entities/pages_domain.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module API + module Entities + class PagesDomain < Grape::Entity + expose :domain + expose :url + expose :verified?, as: :verified + expose :verification_code, as: :verification_code + expose :enabled_until + expose :auto_ssl_enabled + + expose :certificate, + if: ->(pages_domain, _) { pages_domain.certificate? }, + using: Entities::PagesDomainCertificate do |pages_domain| + pages_domain + end + end + end +end diff --git a/lib/api/entities/pages_domain_basic.rb b/lib/api/entities/pages_domain_basic.rb new file mode 100644 index 00000000000..6f8901fe715 --- /dev/null +++ b/lib/api/entities/pages_domain_basic.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module API + module Entities + class PagesDomainBasic < Grape::Entity + expose :domain + expose :url + expose :project_id + expose :verified?, as: :verified + expose :verification_code, as: :verification_code + expose :enabled_until + expose :auto_ssl_enabled + + expose :certificate, + as: :certificate_expiration, + if: ->(pages_domain, _) { pages_domain.certificate? }, + using: Entities::PagesDomainCertificateExpiration do |pages_domain| + pages_domain + end + end + end +end diff --git a/lib/api/entities/pages_domain_certificate.rb b/lib/api/entities/pages_domain_certificate.rb new file mode 100644 index 00000000000..82c4729d454 --- /dev/null +++ b/lib/api/entities/pages_domain_certificate.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class PagesDomainCertificate < Grape::Entity + expose :subject + expose :expired?, as: :expired + expose :certificate + expose :certificate_text + end + end +end diff --git a/lib/api/entities/pages_domain_certificate_expiration.rb b/lib/api/entities/pages_domain_certificate_expiration.rb new file mode 100644 index 00000000000..bfc70f6657f --- /dev/null +++ b/lib/api/entities/pages_domain_certificate_expiration.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class PagesDomainCertificateExpiration < Grape::Entity + expose :expired?, as: :expired + expose :expiration + end + end +end diff --git a/lib/api/entities/personal_access_token.rb b/lib/api/entities/personal_access_token.rb new file mode 100644 index 00000000000..d6fb9af6ab3 --- /dev/null +++ b/lib/api/entities/personal_access_token.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class PersonalAccessToken < Grape::Entity + expose :id, :name, :revoked, :created_at, :scopes + expose :active?, as: :active + expose :expires_at do |personal_access_token| + personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil + end + end + end +end diff --git a/lib/api/entities/personal_access_token_with_token.rb b/lib/api/entities/personal_access_token_with_token.rb new file mode 100644 index 00000000000..84dcd3bc8d8 --- /dev/null +++ b/lib/api/entities/personal_access_token_with_token.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class PersonalAccessTokenWithToken < Entities::PersonalAccessToken + expose :token + end + end +end diff --git a/lib/api/entities/personal_snippet.rb b/lib/api/entities/personal_snippet.rb new file mode 100644 index 00000000000..eb0266e61e6 --- /dev/null +++ b/lib/api/entities/personal_snippet.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class PersonalSnippet < Snippet + expose :raw_url do |snippet| + Gitlab::UrlBuilder.build(snippet, raw: true) + end + end + end +end diff --git a/lib/api/entities/pipeline.rb b/lib/api/entities/pipeline.rb new file mode 100644 index 00000000000..778efbe4bcc --- /dev/null +++ b/lib/api/entities/pipeline.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + class Pipeline < Entities::PipelineBasic + expose :before_sha, :tag, :yaml_errors + + expose :user, with: Entities::UserBasic + expose :created_at, :updated_at, :started_at, :finished_at, :committed_at + expose :duration + expose :coverage + expose :detailed_status, using: DetailedStatusEntity do |pipeline, options| + pipeline.detailed_status(options[:current_user]) + end + end + end +end diff --git a/lib/api/entities/pipeline_basic.rb b/lib/api/entities/pipeline_basic.rb new file mode 100644 index 00000000000..359f6a447ab --- /dev/null +++ b/lib/api/entities/pipeline_basic.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class PipelineBasic < Grape::Entity + expose :id, :sha, :ref, :status + expose :created_at, :updated_at + + expose :web_url do |pipeline, _options| + Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline) + end + end + end +end diff --git a/lib/api/entities/pipeline_schedule.rb b/lib/api/entities/pipeline_schedule.rb new file mode 100644 index 00000000000..a72fe3f3141 --- /dev/null +++ b/lib/api/entities/pipeline_schedule.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class PipelineSchedule < Grape::Entity + expose :id + expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active + expose :created_at, :updated_at + expose :owner, using: Entities::UserBasic + end + end +end diff --git a/lib/api/entities/pipeline_schedule_details.rb b/lib/api/entities/pipeline_schedule_details.rb new file mode 100644 index 00000000000..5e54489a0f9 --- /dev/null +++ b/lib/api/entities/pipeline_schedule_details.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class PipelineScheduleDetails < Entities::PipelineSchedule + expose :last_pipeline, using: Entities::PipelineBasic + expose :variables, using: Entities::Variable + end + end +end diff --git a/lib/api/entities/platform/kubernetes.rb b/lib/api/entities/platform/kubernetes.rb new file mode 100644 index 00000000000..eeb6d57bb8f --- /dev/null +++ b/lib/api/entities/platform/kubernetes.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Platform + class Kubernetes < Grape::Entity + expose :api_url + expose :namespace + expose :authorization_type + expose :ca_cert + end + end + end +end diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb new file mode 100644 index 00000000000..6ed2ed34360 --- /dev/null +++ b/lib/api/entities/project.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module API + module Entities + class Project < BasicProjectDetails + include ::API::Helpers::RelatedResourcesHelpers + + expose :_links do + expose :self do |project| + expose_url(api_v4_projects_path(id: project.id)) + end + + expose :issues, if: -> (project, options) { issues_available?(project, options) } do |project| + expose_url(api_v4_projects_issues_path(id: project.id)) + end + + expose :merge_requests, if: -> (project, options) { mrs_available?(project, options) } do |project| + expose_url(api_v4_projects_merge_requests_path(id: project.id)) + end + + expose :repo_branches do |project| + expose_url(api_v4_projects_repository_branches_path(id: project.id)) + end + + expose :labels do |project| + expose_url(api_v4_projects_labels_path(id: project.id)) + end + + expose :events do |project| + expose_url(api_v4_projects_events_path(id: project.id)) + end + + expose :members do |project| + expose_url(api_v4_projects_members_path(id: project.id)) + end + end + + expose :empty_repo?, as: :empty_repo + expose :archived?, as: :archived + expose :visibility + expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } + expose :resolve_outdated_diff_discussions + expose :container_registry_enabled + expose :container_expiration_policy, using: Entities::ContainerExpirationPolicy, + if: -> (project, _) { project.container_expiration_policy } + + # Expose old field names with the new permissions methods to keep API compatible + # TODO: remove in API v5, replaced by *_access_level + expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } + expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } + expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } + expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } + expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } + + expose(:can_create_merge_request_in) do |project, options| + Ability.allowed?(options[:current_user], :create_merge_request_in, project) + end + + expose(:issues_access_level) { |project, options| project.project_feature.string_access_level(:issues) } + expose(:repository_access_level) { |project, options| project.project_feature.string_access_level(:repository) } + expose(:merge_requests_access_level) { |project, options| project.project_feature.string_access_level(:merge_requests) } + expose(:wiki_access_level) { |project, options| project.project_feature.string_access_level(:wiki) } + expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) } + expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) } + expose(:pages_access_level) { |project, options| project.project_feature.string_access_level(:pages) } + + expose :emails_disabled + expose :shared_runners_enabled + expose :lfs_enabled?, as: :lfs_enabled + expose :creator_id + expose :forked_from_project, using: Entities::BasicProjectDetails, if: ->(project, options) do + project.forked? && Ability.allowed?(options[:current_user], :read_project, project.forked_from_project) + end + expose :import_status + + expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] } do |project| + project.import_state&.last_error + end + + expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } + expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } + expose :ci_default_git_depth + expose :public_builds, as: :public_jobs + expose :build_git_strategy, if: lambda { |project, options| options[:user_can_admin_project] } do |project, options| + project.build_allow_git_fetch ? 'fetch' : 'clone' + end + expose :build_timeout + expose :auto_cancel_pending_pipelines + expose :build_coverage_regex + expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } + expose :shared_with_groups do |project, options| + SharedGroup.represent(project.project_group_links, options) + end + expose :only_allow_merge_if_pipeline_succeeds + expose :request_access_enabled + expose :only_allow_merge_if_all_discussions_are_resolved + expose :remove_source_branch_after_merge + expose :printing_merge_request_link_enabled + expose :merge_method + expose :suggestion_commit_message + expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) { + options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project) + } + expose :auto_devops_enabled?, as: :auto_devops_enabled + expose :auto_devops_deploy_strategy do |project, options| + project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy + end + expose :autoclose_referenced_issues + + # rubocop: disable CodeReuse/ActiveRecord + def self.preload_relation(projects_relation, options = {}) + # Preloading tags, should be done with using only `:tags`, + # as `:tags` are defined as: `has_many :tags, through: :taggings` + # N+1 is solved then by using `subject.tags.map(&:name)` + # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/20555 + super(projects_relation).preload(:group) + .preload(:ci_cd_settings) + .preload(:container_expiration_policy) + .preload(:auto_devops) + .preload(project_group_links: { group: :route }, + fork_network: :root_project, + fork_network_member: :forked_from_project, + forked_from_project: [:route, :forks, :tags, namespace: :route]) + end + # rubocop: enable CodeReuse/ActiveRecord + + def self.forks_counting_projects(projects_relation) + projects_relation + projects_relation.map(&:forked_from_project).compact + end + end + end +end + +API::Entities::Project.prepend_if_ee('EE::API::Entities::Project', with_descendants: true) diff --git a/lib/api/entities/project_access.rb b/lib/api/entities/project_access.rb new file mode 100644 index 00000000000..29f85fda620 --- /dev/null +++ b/lib/api/entities/project_access.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectAccess < Entities::MemberAccess + end + end +end diff --git a/lib/api/entities/project_daily_fetches.rb b/lib/api/entities/project_daily_fetches.rb new file mode 100644 index 00000000000..036b5dc99b8 --- /dev/null +++ b/lib/api/entities/project_daily_fetches.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectDailyFetches < Grape::Entity + expose :fetch_count, as: :count + expose :date + end + end +end diff --git a/lib/api/entities/project_daily_statistics.rb b/lib/api/entities/project_daily_statistics.rb new file mode 100644 index 00000000000..803ee445851 --- /dev/null +++ b/lib/api/entities/project_daily_statistics.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectDailyStatistics < Grape::Entity + expose :fetches do + expose :total_fetch_count, as: :total + expose :fetches, as: :days, using: ProjectDailyFetches + end + end + end +end diff --git a/lib/api/entities/project_export_status.rb b/lib/api/entities/project_export_status.rb new file mode 100644 index 00000000000..ad84a45996a --- /dev/null +++ b/lib/api/entities/project_export_status.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectExportStatus < ProjectIdentity + include ::API::Helpers::RelatedResourcesHelpers + + expose :export_status + expose :_links, if: lambda { |project, _options| project.export_status == :finished } do + expose :api_url do |project| + expose_url(api_v4_projects_export_download_path(id: project.id)) + end + + expose :web_url do |project| + Gitlab::Routing.url_helpers.download_export_project_url(project) + end + end + end + end +end diff --git a/lib/api/entities/project_group_link.rb b/lib/api/entities/project_group_link.rb new file mode 100644 index 00000000000..89138854e67 --- /dev/null +++ b/lib/api/entities/project_group_link.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectGroupLink < Grape::Entity + expose :id, :project_id, :group_id, :group_access, :expires_at + end + end +end diff --git a/lib/api/entities/project_hook.rb b/lib/api/entities/project_hook.rb new file mode 100644 index 00000000000..cdd3714ed64 --- /dev/null +++ b/lib/api/entities/project_hook.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectHook < Hook + expose :project_id, :issues_events, :confidential_issues_events + expose :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events + expose :job_events + expose :push_events_branch_filter + end + end +end diff --git a/lib/api/entities/project_identity.rb b/lib/api/entities/project_identity.rb new file mode 100644 index 00000000000..2055195eea0 --- /dev/null +++ b/lib/api/entities/project_identity.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectIdentity < Grape::Entity + expose :id, :description + expose :name, :name_with_namespace + expose :path, :path_with_namespace + expose :created_at + end + end +end diff --git a/lib/api/entities/project_import_status.rb b/lib/api/entities/project_import_status.rb new file mode 100644 index 00000000000..0b884b43e9e --- /dev/null +++ b/lib/api/entities/project_import_status.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectImportStatus < ProjectIdentity + expose :import_status + + # TODO: Use `expose_nil` once we upgrade the grape-entity gem + expose :import_error, if: lambda { |project, _ops| project.import_state&.last_error } do |project| + project.import_state.last_error + end + end + end +end diff --git a/lib/api/entities/project_label.rb b/lib/api/entities/project_label.rb new file mode 100644 index 00000000000..b47a9414ddb --- /dev/null +++ b/lib/api/entities/project_label.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectLabel < Entities::Label + expose :priority do |label, options| + label.priority(options[:parent]) + end + expose :is_project_label do |label, options| + label.is_a?(::ProjectLabel) + end + end + end +end diff --git a/lib/api/entities/project_service.rb b/lib/api/entities/project_service.rb new file mode 100644 index 00000000000..947cec1e3cd --- /dev/null +++ b/lib/api/entities/project_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectService < Entities::ProjectServiceBasic + # Expose serialized properties + expose :properties do |service, options| + # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 + if service.data_fields_present? + service.data_fields.as_json.slice(*service.api_field_names) + else + service.properties.slice(*service.api_field_names) + end + end + end + end +end diff --git a/lib/api/entities/project_service_basic.rb b/lib/api/entities/project_service_basic.rb new file mode 100644 index 00000000000..eb97ca69a82 --- /dev/null +++ b/lib/api/entities/project_service_basic.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectServiceBasic < Grape::Entity + expose :id, :title + expose :slug do |service| + service.to_param.dasherize + end + expose :created_at, :updated_at, :active + expose :commit_events, :push_events, :issues_events, :confidential_issues_events + expose :merge_requests_events, :tag_push_events, :note_events + expose :confidential_note_events, :pipeline_events, :wiki_page_events + expose :job_events, :comment_on_event_enabled + end + end +end diff --git a/lib/api/entities/project_snippet.rb b/lib/api/entities/project_snippet.rb new file mode 100644 index 00000000000..8ed87e51375 --- /dev/null +++ b/lib/api/entities/project_snippet.rb @@ -0,0 +1,8 @@ +# frozen_String_literal: true + +module API + module Entities + class ProjectSnippet < Entities::Snippet + end + end +end diff --git a/lib/api/entities/project_statistics.rb b/lib/api/entities/project_statistics.rb new file mode 100644 index 00000000000..e5f6165da31 --- /dev/null +++ b/lib/api/entities/project_statistics.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectStatistics < Grape::Entity + expose :commit_count + expose :storage_size + expose :repository_size + expose :wiki_size + expose :lfs_objects_size + expose :build_artifacts_size, as: :job_artifacts_size + end + end +end diff --git a/lib/api/entities/project_with_access.rb b/lib/api/entities/project_with_access.rb new file mode 100644 index 00000000000..c53a712a879 --- /dev/null +++ b/lib/api/entities/project_with_access.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectWithAccess < Project + expose :permissions do + expose :project_access, using: Entities::ProjectAccess do |project, options| + if options[:project_members] + options[:project_members].find { |member| member.source_id == project.id } + else + project.project_member(options[:current_user]) + end + end + + expose :group_access, using: Entities::GroupAccess do |project, options| + if project.group + if options[:group_members] + options[:group_members].find { |member| member.source_id == project.namespace_id } + else + project.group.highest_group_member(options[:current_user]) + end + end + end + end + + # rubocop: disable CodeReuse/ActiveRecord + def self.preload_relation(projects_relation, options = {}) + relation = super(projects_relation, options) + project_ids = relation.select('projects.id') + namespace_ids = relation.select(:namespace_id) + + options[:project_members] = options[:current_user] + .project_members + .where(source_id: project_ids) + .preload(:source, user: [notification_settings: :source]) + + options[:group_members] = options[:current_user] + .group_members + .where(source_id: namespace_ids) + .preload(:source, user: [notification_settings: :source]) + + relation + end + # rubocop: enable CodeReuse/ActiveRecord + end + end +end diff --git a/lib/api/entities/protected_branch.rb b/lib/api/entities/protected_branch.rb new file mode 100644 index 00000000000..80c8a791053 --- /dev/null +++ b/lib/api/entities/protected_branch.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class ProtectedBranch < Grape::Entity + expose :id + expose :name + expose :push_access_levels, using: Entities::ProtectedRefAccess + expose :merge_access_levels, using: Entities::ProtectedRefAccess + end + end +end + +API::Entities::ProtectedBranch.prepend_if_ee('EE::API::Entities::ProtectedBranch') diff --git a/lib/api/entities/protected_ref_access.rb b/lib/api/entities/protected_ref_access.rb new file mode 100644 index 00000000000..f0185705b06 --- /dev/null +++ b/lib/api/entities/protected_ref_access.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class ProtectedRefAccess < Grape::Entity + expose :access_level + expose :access_level_description do |protected_ref_access| + protected_ref_access.humanize + end + end + end +end + +API::Entities::ProtectedRefAccess.prepend_if_ee('EE::API::Entities::ProtectedRefAccess') diff --git a/lib/api/entities/protected_tag.rb b/lib/api/entities/protected_tag.rb new file mode 100644 index 00000000000..dc397f01af6 --- /dev/null +++ b/lib/api/entities/protected_tag.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class ProtectedTag < Grape::Entity + expose :name + expose :create_access_levels, using: Entities::ProtectedRefAccess + end + end +end diff --git a/lib/api/entities/provider/gcp.rb b/lib/api/entities/provider/gcp.rb new file mode 100644 index 00000000000..85f56a9ac1e --- /dev/null +++ b/lib/api/entities/provider/gcp.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + module Provider + class Gcp < Grape::Entity + expose :cluster_id + expose :status_name + expose :gcp_project_id + expose :zone + expose :machine_type + expose :num_nodes + expose :endpoint + end + end + end +end diff --git a/lib/api/entities/push_event_payload.rb b/lib/api/entities/push_event_payload.rb new file mode 100644 index 00000000000..6aad5f10177 --- /dev/null +++ b/lib/api/entities/push_event_payload.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class PushEventPayload < Grape::Entity + expose :commit_count, :action, :ref_type, :commit_from, :commit_to, :ref, + :commit_title, :ref_count + end + end +end diff --git a/lib/api/entities/release.rb b/lib/api/entities/release.rb new file mode 100644 index 00000000000..dc4b91e594e --- /dev/null +++ b/lib/api/entities/release.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module API + module Entities + class Release < Grape::Entity + include ::API::Helpers::Presentable + + expose :name do |release, _| + can_download_code? ? release.name : "Release-#{release.id}" + end + expose :tag, as: :tag_name, if: ->(_, _) { can_download_code? } + expose :description + expose :description_html do |entity| + MarkupHelper.markdown_field(entity, :description) + end + expose :created_at + expose :released_at + expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? } + expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? } + expose :upcoming_release?, as: :upcoming_release + expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? && can_read_milestone? } + expose :commit_path, expose_nil: false + expose :tag_path, expose_nil: false + expose :evidence_sha, expose_nil: false, if: ->(_, _) { can_download_code? } + expose :assets do + expose :assets_count, as: :count do |release, _| + assets_to_exclude = can_download_code? ? [] : [:sources] + release.assets_count(except: assets_to_exclude) + end + expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_download_code? } + expose :links, using: Entities::Releases::Link do |release, options| + release.links.sorted + end + expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? } + end + expose :_links do + expose :self_url, as: :self, expose_nil: false + expose :merge_requests_url, expose_nil: false + expose :issues_url, expose_nil: false + expose :edit_url, expose_nil: false + end + + private + + def can_download_code? + Ability.allowed?(options[:current_user], :download_code, object.project) + end + + def can_read_milestone? + Ability.allowed?(options[:current_user], :read_milestone, object.project) + end + end + end +end diff --git a/lib/api/entities/releases/link.rb b/lib/api/entities/releases/link.rb new file mode 100644 index 00000000000..6cc01e0e981 --- /dev/null +++ b/lib/api/entities/releases/link.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Releases + class Link < Grape::Entity + expose :id + expose :name + expose :url + expose :external?, as: :external + end + end + end +end diff --git a/lib/api/entities/releases/source.rb b/lib/api/entities/releases/source.rb new file mode 100644 index 00000000000..2b0c8038ddf --- /dev/null +++ b/lib/api/entities/releases/source.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module Releases + class Source < Grape::Entity + expose :format + expose :url + end + end + end +end diff --git a/lib/api/entities/remote_mirror.rb b/lib/api/entities/remote_mirror.rb new file mode 100644 index 00000000000..dde3e9dea99 --- /dev/null +++ b/lib/api/entities/remote_mirror.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + class RemoteMirror < Grape::Entity + expose :id + expose :enabled + expose :safe_url, as: :url + expose :update_status + expose :last_update_at + expose :last_update_started_at + expose :last_successful_update_at + expose :last_error + expose :only_protected_branches + end + end +end diff --git a/lib/api/entities/resource_label_event.rb b/lib/api/entities/resource_label_event.rb new file mode 100644 index 00000000000..890264abf93 --- /dev/null +++ b/lib/api/entities/resource_label_event.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Entities + class ResourceLabelEvent < Grape::Entity + expose :id + expose :user, using: Entities::UserBasic + expose :created_at + expose :resource_type do |event, options| + event.issuable.class.name + end + expose :resource_id do |event, options| + event.issuable.id + end + expose :label, using: Entities::LabelBasic + expose :action + end + end +end diff --git a/lib/api/entities/runner.rb b/lib/api/entities/runner.rb new file mode 100644 index 00000000000..6165b54cddb --- /dev/null +++ b/lib/api/entities/runner.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + class Runner < Grape::Entity + expose :id + expose :description + expose :ip_address + expose :active + expose :instance_type?, as: :is_shared + expose :name + expose :online?, as: :online + expose :status + end + end +end diff --git a/lib/api/entities/runner_details.rb b/lib/api/entities/runner_details.rb new file mode 100644 index 00000000000..17202821e6e --- /dev/null +++ b/lib/api/entities/runner_details.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module API + module Entities + class RunnerDetails < Runner + expose :tag_list + expose :run_untagged + expose :locked + expose :maximum_timeout + expose :access_level + expose :version, :revision, :platform, :architecture + expose :contacted_at + expose :token, if: lambda { |runner, options| options[:current_user].admin? || !runner.instance_type? } + # rubocop: disable CodeReuse/ActiveRecord + expose :projects, with: Entities::BasicProjectDetails do |runner, options| + if options[:current_user].admin? + runner.projects + else + options[:current_user].authorized_projects.where(id: runner.projects) + end + end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord + expose :groups, with: Entities::BasicGroupDetails do |runner, options| + if options[:current_user].admin? + runner.groups + else + options[:current_user].authorized_groups.where(id: runner.groups) + end + end + # rubocop: enable CodeReuse/ActiveRecord + end + end +end diff --git a/lib/api/entities/runner_registration_details.rb b/lib/api/entities/runner_registration_details.rb new file mode 100644 index 00000000000..c8ed88ba10a --- /dev/null +++ b/lib/api/entities/runner_registration_details.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class RunnerRegistrationDetails < Grape::Entity + expose :id, :token + end + end +end diff --git a/lib/api/entities/shared_group.rb b/lib/api/entities/shared_group.rb new file mode 100644 index 00000000000..862e73e07f0 --- /dev/null +++ b/lib/api/entities/shared_group.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Entities + class SharedGroup < Grape::Entity + expose :group_id + expose :group_name do |group_link, options| + group_link.group.name + end + expose :group_full_path do |group_link, options| + group_link.group.full_path + end + expose :group_access, as: :group_access_level + expose :expires_at + end + end +end diff --git a/lib/api/entities/snippet.rb b/lib/api/entities/snippet.rb new file mode 100644 index 00000000000..d92f7b79c28 --- /dev/null +++ b/lib/api/entities/snippet.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + class Snippet < Grape::Entity + expose :id, :title, :file_name, :description, :visibility + expose :author, using: Entities::UserBasic + expose :updated_at, :created_at + expose :project_id + expose :web_url do |snippet| + Gitlab::UrlBuilder.build(snippet) + end + end + end +end diff --git a/lib/api/entities/ssh_key.rb b/lib/api/entities/ssh_key.rb new file mode 100644 index 00000000000..0e2f6ebae8c --- /dev/null +++ b/lib/api/entities/ssh_key.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class SSHKey < Grape::Entity + expose :id, :title, :key, :created_at + end + end +end diff --git a/lib/api/entities/ssh_key_with_user.rb b/lib/api/entities/ssh_key_with_user.rb new file mode 100644 index 00000000000..95559bbf2ac --- /dev/null +++ b/lib/api/entities/ssh_key_with_user.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class SSHKeyWithUser < Entities::SSHKey + expose :user, using: Entities::UserPublic + end + end +end diff --git a/lib/api/entities/suggestion.rb b/lib/api/entities/suggestion.rb new file mode 100644 index 00000000000..59f94099d7f --- /dev/null +++ b/lib/api/entities/suggestion.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + class Suggestion < Grape::Entity + expose :id + expose :from_line + expose :to_line + expose :appliable?, as: :appliable + expose :applied + expose :from_content + expose :to_content + end + end +end diff --git a/lib/api/entities/tag.rb b/lib/api/entities/tag.rb new file mode 100644 index 00000000000..2d3569bb9bb --- /dev/null +++ b/lib/api/entities/tag.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module API + module Entities + class Tag < Grape::Entity + expose :name, :message, :target + + expose :commit, using: Entities::Commit do |repo_tag, options| + options[:project].repository.commit(repo_tag.dereferenced_target) + end + + # rubocop: disable CodeReuse/ActiveRecord + expose :release, using: Entities::TagRelease do |repo_tag, options| + options[:project].releases.find_by(tag: repo_tag.name) + end + # rubocop: enable CodeReuse/ActiveRecord + + expose :protected do |repo_tag, options| + ::ProtectedTag.protected?(options[:project], repo_tag.name) + end + end + end +end diff --git a/lib/api/entities/tag_release.rb b/lib/api/entities/tag_release.rb new file mode 100644 index 00000000000..d5f73d60332 --- /dev/null +++ b/lib/api/entities/tag_release.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + # deprecated old Release representation + class TagRelease < Grape::Entity + expose :tag, as: :tag_name + expose :description + end + end +end diff --git a/lib/api/entities/template.rb b/lib/api/entities/template.rb new file mode 100644 index 00000000000..ef364d971bf --- /dev/null +++ b/lib/api/entities/template.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class Template < Grape::Entity + expose :name, :content + end + end +end diff --git a/lib/api/entities/templates_list.rb b/lib/api/entities/templates_list.rb new file mode 100644 index 00000000000..8e8aa1bd285 --- /dev/null +++ b/lib/api/entities/templates_list.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class TemplatesList < Grape::Entity + expose :key + expose :name + end + end +end diff --git a/lib/api/entities/todo.rb b/lib/api/entities/todo.rb new file mode 100644 index 00000000000..abfdde89bf1 --- /dev/null +++ b/lib/api/entities/todo.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module API + module Entities + class Todo < Grape::Entity + expose :id + expose :project, using: Entities::ProjectIdentity, if: -> (todo, _) { todo.project_id } + expose :group, using: 'API::Entities::NamespaceBasic', if: -> (todo, _) { todo.group_id } + expose :author, using: Entities::UserBasic + expose :action_name + expose :target_type + + expose :target do |todo, options| + todo_options = options.fetch(todo.target_type, {}) + todo_target_class(todo.target_type).represent(todo.target, todo_options) + end + + expose :target_url do |todo, options| + todo_target_url(todo) + end + + expose :body + expose :state + expose :created_at + + def todo_target_class(target_type) + # false as second argument prevents looking up in module hierarchy + # see also https://gitlab.com/gitlab-org/gitlab-foss/issues/59719 + ::API::Entities.const_get(target_type, false) + end + + def todo_target_url(todo) + target_type = todo.target_type.underscore + target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url" + + Gitlab::Routing + .url_helpers + .public_send(target_url, todo.resource_parent, todo.target, anchor: todo_target_anchor(todo)) # rubocop:disable GitlabSecurity/PublicSend + end + + def todo_target_anchor(todo) + "note_#{todo.note_id}" if todo.note_id? + end + end + end +end + +API::Entities::Todo.prepend_if_ee('EE::API::Entities::Todo') diff --git a/lib/api/entities/tree_object.rb b/lib/api/entities/tree_object.rb new file mode 100644 index 00000000000..e4e840ebe43 --- /dev/null +++ b/lib/api/entities/tree_object.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + class TreeObject < Grape::Entity + expose :id, :name, :type, :path + + expose :mode do |obj, options| + filemode = obj.mode + filemode = "0" + filemode if filemode.length < 6 + filemode + end + end + end +end diff --git a/lib/api/entities/trigger.rb b/lib/api/entities/trigger.rb new file mode 100644 index 00000000000..6a9f772fc6b --- /dev/null +++ b/lib/api/entities/trigger.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + class Trigger < Grape::Entity + include ::API::Helpers::Presentable + + expose :id + expose :token + expose :description + expose :created_at, :updated_at, :last_used + expose :owner, using: Entities::UserBasic + end + end +end diff --git a/lib/api/entities/user.rb b/lib/api/entities/user.rb new file mode 100644 index 00000000000..15e4619cdb8 --- /dev/null +++ b/lib/api/entities/user.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class User < UserBasic + expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } + expose :bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization + end + end +end diff --git a/lib/api/entities/user_activity.rb b/lib/api/entities/user_activity.rb new file mode 100644 index 00000000000..30c23cc7a67 --- /dev/null +++ b/lib/api/entities/user_activity.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class UserActivity < Grape::Entity + expose :username + expose :last_activity_on + expose :last_activity_on, as: :last_activity_at # Back-compat + end + end +end diff --git a/lib/api/entities/user_agent_detail.rb b/lib/api/entities/user_agent_detail.rb new file mode 100644 index 00000000000..a2d02c16589 --- /dev/null +++ b/lib/api/entities/user_agent_detail.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class UserAgentDetail < Grape::Entity + expose :user_agent + expose :ip_address + expose :submitted, as: :akismet_submitted + end + end +end diff --git a/lib/api/entities/user_basic.rb b/lib/api/entities/user_basic.rb new file mode 100644 index 00000000000..e063aa42855 --- /dev/null +++ b/lib/api/entities/user_basic.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module API + module Entities + class UserBasic < UserSafe + expose :state + + expose :avatar_url do |user, options| + user.avatar_url(only_path: false) + end + + expose :avatar_path, if: ->(user, options) { options.fetch(:only_path, false) && user.avatar_path } + expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes + + expose :web_url do |user, options| + Gitlab::Routing.url_helpers.user_url(user) + end + end + end +end diff --git a/lib/api/entities/user_details_with_admin.rb b/lib/api/entities/user_details_with_admin.rb new file mode 100644 index 00000000000..9ea5c583437 --- /dev/null +++ b/lib/api/entities/user_details_with_admin.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class UserDetailsWithAdmin < UserWithAdmin + expose :highest_role + expose :current_sign_in_ip + expose :last_sign_in_ip + end + end +end diff --git a/lib/api/entities/user_public.rb b/lib/api/entities/user_public.rb new file mode 100644 index 00000000000..15e9b905bef --- /dev/null +++ b/lib/api/entities/user_public.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module API + module Entities + class UserPublic < Entities::User + expose :last_sign_in_at + expose :confirmed_at + expose :last_activity_on + expose :email + expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at + expose :identities, using: Entities::Identity + expose :can_create_group?, as: :can_create_group + expose :can_create_project?, as: :can_create_project + expose :two_factor_enabled?, as: :two_factor_enabled + expose :external + expose :private_profile + end + end +end + +API::Entities::UserPublic.prepend_if_ee('EE::API::Entities::UserPublic', with_descendants: true) diff --git a/lib/api/entities/user_safe.rb b/lib/api/entities/user_safe.rb new file mode 100644 index 00000000000..feb01767fd6 --- /dev/null +++ b/lib/api/entities/user_safe.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class UserSafe < Grape::Entity + expose :id, :name, :username + end + end +end diff --git a/lib/api/entities/user_stars_project.rb b/lib/api/entities/user_stars_project.rb new file mode 100644 index 00000000000..3e087c17c2d --- /dev/null +++ b/lib/api/entities/user_stars_project.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class UserStarsProject < Grape::Entity + expose :starred_since + expose :user, using: Entities::UserBasic + end + end +end diff --git a/lib/api/entities/user_status.rb b/lib/api/entities/user_status.rb new file mode 100644 index 00000000000..9bc4cbf240f --- /dev/null +++ b/lib/api/entities/user_status.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class UserStatus < Grape::Entity + expose :emoji + expose :message + expose :message_html do |entity| + MarkupHelper.markdown_field(entity, :message) + end + end + end +end diff --git a/lib/api/entities/user_with_admin.rb b/lib/api/entities/user_with_admin.rb new file mode 100644 index 00000000000..d3df12200ff --- /dev/null +++ b/lib/api/entities/user_with_admin.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class UserWithAdmin < UserPublic + expose :admin?, as: :is_admin + end + end +end + +API::Entities::UserWithAdmin.prepend_if_ee('EE::API::Entities::UserWithAdmin', with_descendants: true) diff --git a/lib/api/entities/variable.rb b/lib/api/entities/variable.rb new file mode 100644 index 00000000000..6705df30b2e --- /dev/null +++ b/lib/api/entities/variable.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class Variable < Grape::Entity + expose :variable_type, :key, :value + expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) } + expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) } + expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) } + end + end +end diff --git a/lib/api/entities/wiki_attachment.rb b/lib/api/entities/wiki_attachment.rb new file mode 100644 index 00000000000..e622dea04dd --- /dev/null +++ b/lib/api/entities/wiki_attachment.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module API + module Entities + class WikiAttachment < Grape::Entity + include Gitlab::FileMarkdownLinkBuilder + + expose :file_name + expose :file_path + expose :branch + expose :link do + expose :file_path, as: :url + expose :markdown do |_entity| + self.markdown_link + end + end + + def filename + object.file_name + end + + def secure_url + object.file_path + end + end + end +end diff --git a/lib/api/entities/wiki_page.rb b/lib/api/entities/wiki_page.rb new file mode 100644 index 00000000000..a8ef0bd857c --- /dev/null +++ b/lib/api/entities/wiki_page.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class WikiPage < WikiPageBasic + expose :content + end + end +end diff --git a/lib/api/entities/wiki_page_basic.rb b/lib/api/entities/wiki_page_basic.rb new file mode 100644 index 00000000000..e10c0e6d553 --- /dev/null +++ b/lib/api/entities/wiki_page_basic.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class WikiPageBasic < Grape::Entity + expose :format + expose :slug + expose :title + end + end +end diff --git a/lib/api/error_tracking.rb b/lib/api/error_tracking.rb index f92f1326daa..14888037f53 100644 --- a/lib/api/error_tracking.rb +++ b/lib/api/error_tracking.rb @@ -23,6 +23,34 @@ module API present setting, with: Entities::ErrorTracking::ProjectSetting end + + desc 'Enable or disable error tracking settings for the project' do + detail 'This feature was introduced in GitLab 12.8.' + success Entities::ErrorTracking::ProjectSetting + end + params do + requires :active, type: Boolean, desc: 'Specifying whether to enable or disable error tracking settings', allow_blank: false + end + + patch ':id/error_tracking/settings/' do + authorize! :admin_operations, user_project + + setting = user_project.error_tracking_setting + + not_found!('Error Tracking Setting') unless setting + + update_params = { + error_tracking_setting_attributes: { enabled: params[:active] } + } + + result = ::Projects::Operations::UpdateService.new(user_project, current_user, update_params).execute + + if result[:status] == :success + present setting, with: Entities::ErrorTracking::ProjectSetting + else + result + end + end end end end diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb index f7ef0cfd0d8..88d04e70e11 100644 --- a/lib/api/group_boards.rb +++ b/lib/api/group_boards.rb @@ -28,6 +28,7 @@ module API success ::API::Entities::Board end get '/:board_id' do + authorize!(:read_board, user_group) present board, with: ::API::Entities::Board end @@ -39,6 +40,7 @@ module API use :pagination end get '/' do + authorize!(:read_board, user_group) present paginate(board_parent.boards.with_associations), with: Entities::Board end end @@ -55,6 +57,7 @@ module API use :pagination end get '/lists' do + authorize!(:read_board, user_group) present paginate(board_lists), with: Entities::List end @@ -66,6 +69,7 @@ module API requires :list_id, type: Integer, desc: 'The ID of a list' end get '/lists/:list_id' do + authorize!(:read_board, user_group) present board_lists.find(params[:list_id]), with: Entities::List end diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb index abfe10b7fa1..0108f6feae3 100644 --- a/lib/api/group_clusters.rb +++ b/lib/api/group_clusters.rb @@ -59,7 +59,7 @@ module API requires :token, type: String, desc: 'Token to authenticate against Kubernetes' optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)' optional :namespace, type: String, desc: 'Unique namespace related to Group' - optional :authorization_type, type: String, values: Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC' + optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC' end use :create_params_ee end @@ -96,7 +96,7 @@ module API put ':id/clusters/:cluster_id' do authorize! :update_cluster, cluster - update_service = Clusters::UpdateService.new(current_user, update_cluster_params) + update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params) if update_service.execute(cluster) present cluster, with: Entities::ClusterGroup diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb index 8025a16e191..6fe72458da2 100644 --- a/lib/api/group_export.rb +++ b/lib/api/group_export.rb @@ -3,6 +3,8 @@ module API class GroupExport < Grape::API before do + not_found! unless Feature.enabled?(:group_import_export, user_group, default_enabled: true) + authorize! :admin_group, user_group end @@ -25,7 +27,7 @@ module API detail 'This feature was introduced in GitLab 12.5.' end post ':id/export' do - GroupExportWorker.perform_async(current_user.id, user_group.id, params) + GroupExportWorker.perform_async(current_user.id, user_group.id, params) # rubocop:disable CodeReuse/Worker accepted! end diff --git a/lib/api/group_import.rb b/lib/api/group_import.rb new file mode 100644 index 00000000000..ed52506de14 --- /dev/null +++ b/lib/api/group_import.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module API + class GroupImport < Grape::API + MAXIMUM_FILE_SIZE = 50.megabytes.freeze + + helpers do + def parent_group + find_group!(params[:parent_id]) if params[:parent_id].present? + end + + def authorize_create_group! + if parent_group + authorize! :create_subgroup, parent_group + else + authorize! :create_group + end + end + + def closest_allowed_visibility_level + if parent_group + Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level) + else + Gitlab::VisibilityLevel::PRIVATE + end + end + end + + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Workhorse authorize the group import upload' do + detail 'This feature was introduced in GitLab 12.8' + end + post 'import/authorize' do + require_gitlab_workhorse! + + Gitlab::Workhorse.verify_api_request!(headers) + + status 200 + content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE + + ImportExportUploader.workhorse_authorize(has_length: false, maximum_size: MAXIMUM_FILE_SIZE) + end + + desc 'Create a new group import' do + detail 'This feature was introduced in GitLab 12.8' + success Entities::Group + end + params do + requires :path, type: String, desc: 'Group path' + requires :name, type: String, desc: 'Group name' + optional :parent_id, type: Integer, desc: "The ID of the parent group that the group will be imported into. Defaults to the current user's namespace." + 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 + post 'import' do + authorize_create_group! + require_gitlab_workhorse! + + uploaded_file = UploadedFile.from_params(params, :file, ImportExportUploader.workhorse_local_upload_path) + + bad_request!('Unable to process group import file') unless uploaded_file + + group_params = { + path: params[:path], + name: params[:name], + parent_id: params[:parent_id], + visibility_level: closest_allowed_visibility_level, + import_export_upload: ImportExportUpload.new(import_file: uploaded_file) + } + + group = ::Groups::CreateService.new(current_user, group_params).execute + + if group.persisted? + GroupImportWorker.perform_async(current_user.id, group.id) # rubocop:disable CodeReuse/Worker + + accepted! + else + render_api_error!("Failed to save group #{group.errors.messages}", 400) + end + end + end + end +end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 52fa3f8a68e..d375c35e8c0 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -92,6 +92,15 @@ module API present paginate(groups), options end + + def delete_group(group) + Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/46285') + destroy_conditionally!(group) do |group| + ::Groups::DestroyService.new(group, current_user).async_execute + end + + accepted! + end end resource :groups do @@ -187,12 +196,7 @@ module API group = find_group!(params[:id]) authorize! :admin_group, group - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/46285') - destroy_conditionally!(group) do |group| - ::Groups::DestroyService.new(group, current_user).async_execute - end - - accepted! + delete_group(group) end desc 'Get a list of projects in this group.' do diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 7d9a91cd360..001fb92ec52 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -326,7 +326,7 @@ module API def order_options_with_tie_breaker order_options = { params[:order_by] => params[:sort] } - order_options['id'] ||= 'desc' + order_options['id'] ||= params[:sort] || 'asc' order_options end @@ -444,7 +444,7 @@ module API def present_disk_file!(path, filename, content_type = 'application/octet-stream') filename ||= File.basename(path) - header['Content-Disposition'] = ::Gitlab::ContentDisposition.format(disposition: 'attachment', filename: filename) + header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: filename) header['Content-Transfer-Encoding'] = 'binary' content_type content_type @@ -552,7 +552,7 @@ module API def send_git_blob(repository, blob) env['api.format'] = :txt content_type 'text/plain' - header['Content-Disposition'] = ::Gitlab::ContentDisposition.format(disposition: 'inline', filename: blob.name) + header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'inline', filename: blob.name) # Let Workhorse examine the content and determine the better content disposition header[Gitlab::Workhorse::DETECT_HEADER] = "true" diff --git a/lib/api/helpers/file_upload_helpers.rb b/lib/api/helpers/file_upload_helpers.rb new file mode 100644 index 00000000000..c5fb291a2b7 --- /dev/null +++ b/lib/api/helpers/file_upload_helpers.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Helpers + module FileUploadHelpers + def file_is_valid? + params[:file] && params[:file]['tempfile'].respond_to?(:read) + end + + def validate_file! + render_api_error!('Uploaded file is invalid', 400) unless file_is_valid? + end + end + end +end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index cc4a0d348a0..ab43096a1de 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -55,30 +55,6 @@ module API ::Users::ActivityService.new(actor).execute if commands.include?(params[:action]) end - def merge_request_urls - ::MergeRequests::GetUrlsService.new(project).execute(params[:changes]) - end - - def process_mr_push_options(push_options, project, user, changes) - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/61359') - - service = ::MergeRequests::PushOptionsHandlerService.new( - project, - user, - changes, - push_options - ).execute - - if service.errors.present? - push_options_warning(service.errors.join("\n\n")) - end - end - - def push_options_warning(warning) - options = Array.wrap(params[:push_options]).map { |p| "'#{p}'" }.join(' ') - "WARNINGS:\nError encountered with push options #{options}: #{warning}" - end - def redis_ping result = Gitlab::Redis::SharedState.with { |redis| redis.ping } @@ -104,21 +80,19 @@ module API # rubocop:disable Gitlab/ModuleWithInstanceVariables def set_project - if params[:gl_repository] - @project, @repo_type = Gitlab::GlRepository.parse(params[:gl_repository]) - @redirected_path = nil - elsif params[:project] - @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse(params[:project]) - else - @project, @repo_type, @redirected_path = nil, nil, nil - end + @project, @repo_type, @redirected_path = + if params[:gl_repository] + Gitlab::GlRepository.parse(params[:gl_repository]) + elsif params[:project] + Gitlab::RepoPath.parse(params[:project]) + end end # rubocop:enable Gitlab/ModuleWithInstanceVariables # Project id to pass between components that don't share/don't have # access to the same filesystem mounts def gl_repository - repo_type.identifier_for_subject(project) + repo_type.identifier_for_container(project) end def gl_project_path diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb index d06c59907b4..5cc435e6801 100644 --- a/lib/api/helpers/members_helpers.rb +++ b/lib/api/helpers/members_helpers.rb @@ -46,7 +46,7 @@ module API end def present_members(members) - present members, with: Entities::Member, current_user: current_user + present members, with: Entities::Member, current_user: current_user, show_seat_info: params[:show_seat_info] end end end diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index 8adfac346f6..3c453953e37 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -72,18 +72,26 @@ module API end def noteable_read_ability_name(noteable) - "read_#{noteable.class.to_s.underscore}".to_sym + "read_#{ability_name(noteable)}".to_sym end - def find_noteable(parent_type, parent_id, noteable_type, noteable_id) - params = finder_params_by_noteable_type_and_id(noteable_type, noteable_id, parent_id) + def ability_name(noteable) + if noteable.respond_to?(:to_ability_name) + noteable.to_ability_name + else + noteable.class.to_s.underscore + end + end + + def find_noteable(noteable_type, noteable_id) + params = finder_params_by_noteable_type_and_id(noteable_type, noteable_id) noteable = NotesFinder.new(current_user, params).target noteable = nil unless can?(current_user, noteable_read_ability_name(noteable), noteable) noteable || not_found!(noteable_type) end - def finder_params_by_noteable_type_and_id(type, id, parent_id) + def finder_params_by_noteable_type_and_id(type, id) target_type = type.name.underscore { target_type: target_type }.tap do |h| if %w(issue merge_request).include?(target_type) @@ -92,11 +100,11 @@ module API h[:target_id] = id end - add_parent_to_finder_params(h, type, parent_id) + add_parent_to_finder_params(h, type) end end - def add_parent_to_finder_params(finder_params, noteable_type, parent_id) + def add_parent_to_finder_params(finder_params, noteable_type) finder_params[:project] = user_project end diff --git a/lib/api/helpers/pagination_strategies.rb b/lib/api/helpers/pagination_strategies.rb index 5f63635297a..6bebb4bfeac 100644 --- a/lib/api/helpers/pagination_strategies.rb +++ b/lib/api/helpers/pagination_strategies.rb @@ -26,7 +26,7 @@ module API private def keyset_pagination_enabled? - params[:pagination] == 'keyset' && Feature.enabled?(:api_keyset_pagination, default_enabled: true) + params[:pagination] == 'keyset' end end end diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 6333e00daf5..c7c9f3ba077 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -27,7 +27,9 @@ module API optional :wiki_access_level, type: String, values: %w(disabled private enabled), desc: 'Wiki access level. One of `disabled`, `private` or `enabled`' optional :builds_access_level, type: String, values: %w(disabled private enabled), desc: 'Builds access level. One of `disabled`, `private` or `enabled`' optional :snippets_access_level, type: String, values: %w(disabled private enabled), desc: 'Snippets access level. One of `disabled`, `private` or `enabled`' + optional :pages_access_level, type: String, values: %w(disabled private enabled public), desc: 'Pages access level. One of `disabled`, `private`, `enabled` or `public`' + optional :emails_disabled, type: Boolean, desc: 'Disable email notifications' optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push' optional :remove_source_branch_after_merge, type: Boolean, desc: 'Remove the source branch by default after merge' @@ -86,6 +88,7 @@ module API def self.update_params_at_least_one_of [ + :autoclose_referenced_issues, :auto_devops_enabled, :auto_devops_deploy_strategy, :auto_cancel_pending_pipelines, @@ -99,7 +102,7 @@ module API :container_expiration_policy_attributes, :default_branch, :description, - :autoclose_referenced_issues, + :emails_disabled, :issues_access_level, :lfs_enabled, :merge_requests_access_level, @@ -107,6 +110,7 @@ module API :name, :only_allow_merge_if_all_discussions_are_resolved, :only_allow_merge_if_pipeline_succeeds, + :pages_access_level, :path, :printing_merge_request_link_enabled, :public_builds, diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index fa8b9ad79bd..1f1253c8542 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -45,7 +45,7 @@ module API end def authenticate_job! - job = Ci::Build.find_by_id(params[:id]) + job = current_job validate_job!(job) do forbidden! unless job_token_valid?(job) @@ -54,6 +54,10 @@ module API job end + def current_job + @current_job ||= Ci::Build.find_by_id(params[:id]) + end + def job_token_valid?(job) token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s token && job.valid_token?(token) diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb index c02244c7202..4c44aca2de4 100644 --- a/lib/api/helpers/services_helpers.rb +++ b/lib/api/helpers/services_helpers.rb @@ -161,6 +161,7 @@ module API def self.services { + 'alerts' => [], 'asana' => [ { required: true, @@ -675,6 +676,12 @@ module API type: String, desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…' }, + { + required: false, + name: :branches_to_be_notified, + type: String, + desc: 'Branches for which notifications are to be sent' + }, chat_notification_flags ].flatten, 'mattermost' => [ @@ -723,6 +730,7 @@ module API def self.service_classes [ + ::AlertsService, ::AsanaService, ::AssemblaService, ::BambooService, diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index d64de2bb465..382bbeb66de 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -9,7 +9,8 @@ module API before do Gitlab::ApplicationContext.push( user: -> { actor&.user }, - project: -> { project } + project: -> { project }, + caller_id: route.origin ) end @@ -211,40 +212,7 @@ module API post '/post_receive' do status 200 - response = Gitlab::InternalPostReceive::Response.new - - # Try to load the project and users so we have the application context - # available for logging before we schedule any jobs. - user = actor.user - project - - push_options = Gitlab::PushOptions.new(params[:push_options]) - - response.reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease - - PostReceive.perform_async(params[:gl_repository], params[:identifier], - params[:changes], push_options.as_json) - - mr_options = push_options.get(:merge_request) - if mr_options.present? - message = process_mr_push_options(mr_options, project, user, params[:changes]) - response.add_alert_message(message) - end - - broadcast_message = BroadcastMessage.current&.last&.message - response.add_alert_message(broadcast_message) - - response.add_merge_request_urls(merge_request_urls) - - # Neither User nor Project are guaranteed to be returned; an orphaned write deploy - # key could be used - if user && project - redirect_message = Gitlab::Checks::ProjectMoved.fetch_message(user.id, project.id) - project_created_message = Gitlab::Checks::ProjectCreated.fetch_message(user.id, project.id) - - response.add_basic_message(redirect_message) - response.add_basic_message(project_created_message) - end + response = PostReceiveService.new(actor.user, project, params).execute ee_post_receive_response_hook(response) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 4e21815fa35..e5bfca13d66 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -120,6 +120,7 @@ module API end params do use :issues_params + optional :non_archived, type: Boolean, desc: 'Return issues from non archived projects', default: true end get ":id/issues" do issues = paginate(find_issues(group_id: user_group.id, include_subgroups: true)) diff --git a/lib/api/keys.rb b/lib/api/keys.rb index bec3dc9bd97..b730e027063 100644 --- a/lib/api/keys.rb +++ b/lib/api/keys.rb @@ -26,7 +26,7 @@ module API get do authenticated_with_can_read_all_resources! - key = KeysFinder.new(current_user, params).execute + key = KeysFinder.new(params).execute not_found!('Key') unless key diff --git a/lib/api/lsif_data.rb b/lib/api/lsif_data.rb new file mode 100644 index 00000000000..63e6eb3ab2d --- /dev/null +++ b/lib/api/lsif_data.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module API + class LsifData < Grape::API + MAX_FILE_SIZE = 10.megabytes + + before do + not_found! if Feature.disabled?(:code_navigation, user_project) + end + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :commit_id, type: String, desc: 'The ID of a commit' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + segment ':id/commits/:commit_id' do + params do + requires :path, type: String, desc: 'The path of a file' + end + get 'lsif/info' do + authorize! :download_code, user_project + + artifact = + @project.job_artifacts + .with_file_types(['lsif']) + .for_sha(params[:commit_id]) + .last + + not_found! unless artifact + authorize! :read_pipeline, artifact.job.pipeline + file_too_large! if artifact.file.cached_size > MAX_FILE_SIZE + + ::Projects::LsifDataService.new(artifact.file, @project, params).execute + end + end + end + end +end diff --git a/lib/api/members.rb b/lib/api/members.rb index e4df2f341c6..2e49b4be45c 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -19,6 +19,7 @@ module API params do optional :query, type: String, desc: 'A query string to search for members' optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership' + optional :show_seat_info, type: Boolean, desc: 'Show seat information for members' use :optional_filter_params_ee use :pagination end @@ -37,6 +38,7 @@ module API params do optional :query, type: String, desc: 'A query string to search for members' optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership' + optional :show_seat_info, type: Boolean, desc: 'Show seat information for members' use :pagination end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index bd857278ee5..2b1bcc855d2 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -4,6 +4,8 @@ module API class MergeRequests < Grape::API include PaginationParams + CONTEXT_COMMITS_POST_LIMIT = 20 + before { authenticate_non_get! } helpers ::Gitlab::IssuableMetadata @@ -139,6 +141,8 @@ module API end params do use :merge_requests_params + optional :non_archived, type: Boolean, desc: 'Return merge requests from non archived projects', + default: true end get ":id/merge_requests" do merge_requests = find_merge_requests(group_id: user_group.id, include_subgroups: true) @@ -290,6 +294,74 @@ module API present commits, with: Entities::Commit end + desc 'Get the context commits of a merge request' do + success Entities::Commit + end + get ':id/merge_requests/:merge_request_iid/context_commits' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) + project = merge_request.project + + not_found! unless project.context_commits_enabled? + + context_commits = + paginate(merge_request.merge_request_context_commits).map(&:to_commit) + + present context_commits, with: Entities::Commit + end + + params do + requires :commits, type: Array, allow_blank: false, desc: 'List of context commits sha' + end + desc 'create context commits of merge request' do + success Entities::Commit + end + post ':id/merge_requests/:merge_request_iid/context_commits' do + commit_ids = params[:commits] + + if commit_ids.size > CONTEXT_COMMITS_POST_LIMIT + render_api_error!("Context commits array size should not be more than #{CONTEXT_COMMITS_POST_LIMIT}", 400) + end + + merge_request = find_merge_request_with_access(params[:merge_request_iid]) + project = merge_request.project + + not_found! unless project.context_commits_enabled? + + authorize!(:update_merge_request, merge_request) + + project = merge_request.target_project + result = ::MergeRequests::AddContextService.new(project, current_user, merge_request: merge_request, commits: commit_ids).execute + + if result.instance_of?(Array) + present result, with: Entities::Commit + else + render_api_error!(result[:message], result[:http_status]) + end + end + + params do + requires :commits, type: Array, allow_blank: false, desc: 'List of context commits sha' + end + desc 'remove context commits of merge request' + delete ':id/merge_requests/:merge_request_iid/context_commits' do + commit_ids = params[:commits] + merge_request = find_merge_request_with_access(params[:merge_request_iid]) + project = merge_request.project + + not_found! unless project.context_commits_enabled? + + authorize!(:destroy_merge_request, merge_request) + project = merge_request.target_project + commits = project.repository.commits_by(oids: commit_ids) + + if commits.size != commit_ids.size + render_api_error!("One or more context commits' sha is not valid.", 400) + end + + MergeRequestContextCommit.delete_bulk(merge_request, commits) + status 204 + end + desc 'Show the merge request changes' do success Entities::MergeRequestChanges end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 9575e8e9f36..35eda481a4f 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -30,7 +30,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord get ":id/#{noteables_str}/:noteable_id/notes" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) # We exclude notes that are cross-references and that cannot be viewed # by the current user. By doing this exclusion at this level and not @@ -58,7 +58,7 @@ module API requires :noteable_id, type: Integer, desc: 'The ID of the noteable' end get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) get_note(noteable, params[:note_id]) end @@ -71,7 +71,7 @@ module API optional :created_at, type: String, desc: 'The creation date of the note' end post ":id/#{noteables_str}/:noteable_id/notes" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) opts = { note: params[:body], @@ -98,7 +98,7 @@ module API requires :body, type: String, desc: 'The content of a note' end put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) update_note(noteable, params[:note_id]) end @@ -111,7 +111,7 @@ module API requires :note_id, type: Integer, desc: 'The ID of a note' end delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) + noteable = find_noteable(noteable_type, params[:noteable_id]) delete_note(noteable, params[:note_id]) end diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb index 1d1ef1afc6b..445a37a70c0 100644 --- a/lib/api/pipeline_schedules.rb +++ b/lib/api/pipeline_schedules.rb @@ -111,6 +111,25 @@ module API destroy_conditionally!(pipeline_schedule) end + desc 'Play a scheduled pipeline immediately' do + detail 'This feature was added in GitLab 12.8' + end + params do + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + end + post ':id/pipeline_schedules/:pipeline_schedule_id/play' do + authorize! :play_pipeline_schedule, pipeline_schedule + + job_id = RunPipelineScheduleWorker # rubocop:disable CodeReuse/Worker + .perform_async(pipeline_schedule.id, current_user.id) + + if job_id + created! + else + render_api_error!('Unable to schedule pipeline run immediately', 500) + end + end + desc 'Create a new pipeline schedule variable' do success Entities::Variable end diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb index 8e35914f48a..b482980b88a 100644 --- a/lib/api/project_clusters.rb +++ b/lib/api/project_clusters.rb @@ -62,7 +62,7 @@ module API requires :token, type: String, desc: 'Token to authenticate against Kubernetes' optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)' optional :namespace, type: String, desc: 'Unique namespace related to Project' - optional :authorization_type, type: String, values: Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC' + optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC' end use :create_params_ee end @@ -100,7 +100,7 @@ module API put ':id/clusters/:cluster_id' do authorize! :update_cluster, cluster - update_service = Clusters::UpdateService.new(current_user, update_cluster_params) + update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params) if update_service.execute(cluster) present cluster, with: Entities::ClusterProject diff --git a/lib/api/project_container_repositories.rb b/lib/api/project_container_repositories.rb index 2b33069e324..70c913bea98 100644 --- a/lib/api/project_container_repositories.rb +++ b/lib/api/project_container_repositories.rb @@ -41,7 +41,7 @@ module API delete ':id/registry/repositories/:repository_id', requirements: REPOSITORY_ENDPOINT_REQUIREMENTS do authorize_admin_container_image! - DeleteContainerRepositoryWorker.perform_async(current_user.id, repository.id) + DeleteContainerRepositoryWorker.perform_async(current_user.id, repository.id) # rubocop:disable CodeReuse/Worker track_event('delete_repository') status :accepted @@ -79,8 +79,10 @@ module API message = 'This request has already been made. You can run this at most once an hour for a given container repository' render_api_error!(message, 400) unless obtain_new_cleanup_container_lease + # rubocop:disable CodeReuse/Worker CleanupContainerRepositoryWorker.perform_async(current_user.id, repository.id, - declared_params.except(:repository_id)) + declared_params.except(:repository_id).merge(container_expiration_policy: false)) + # rubocop:enable CodeReuse/Worker track_event('delete_tag_bulk') diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index b3f17447ea0..ea793a09f6c 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -5,18 +5,19 @@ module API include PaginationParams helpers Helpers::ProjectsHelpers + helpers Helpers::FileUploadHelpers helpers do def import_params declared_params(include_missing: false) end - def file_is_valid? - import_params[:file] && import_params[:file]['tempfile'].respond_to?(:read) + def throttled?(key, scope) + rate_limiter.throttled?(key, scope: scope) end - def validate_file! - render_api_error!('The file is invalid', 400) unless file_is_valid? + def rate_limiter + ::Gitlab::ApplicationRateLimiter end end @@ -43,6 +44,14 @@ module API success Entities::ProjectImportStatus end post 'import' do + key = "project_import".to_sym + + if throttled?(key, [current_user, key]) + rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user) + + render_api_error!({ error: _('This endpoint has been requested too many times. Try again later.') }, 429) + end + validate_file! Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42437') diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index ecada843972..3040c3c27c6 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -60,7 +60,7 @@ module API mutually_exclusive :code, :content end post ":id/snippets" do - authorize! :create_project_snippet, user_project + authorize! :create_snippet, user_project snippet_params = declared_params(include_missing: false).merge(request: request, api: true) snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present? @@ -97,7 +97,7 @@ module API snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id)) not_found!('Snippet') unless snippet - authorize! :update_project_snippet, snippet + authorize! :update_snippet, snippet snippet_params = declared_params(include_missing: false) .merge(request: request, api: true) @@ -126,7 +126,7 @@ module API snippet = snippets_for_current_user.find_by(id: params[:snippet_id]) not_found!('Snippet') unless snippet - authorize! :admin_project_snippet, snippet + authorize! :admin_snippet, snippet destroy_conditionally!(snippet) do |snippet| service = ::Snippets::DestroyService.new(current_user, snippet) diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index c7665c20234..1fd86d1e720 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -19,10 +19,15 @@ module API end params do use :pagination + optional :search, type: String, desc: 'Search for a protected branch by name' end # rubocop: disable CodeReuse/ActiveRecord get ':id/protected_branches' do - protected_branches = user_project.protected_branches.preload(:push_access_levels, :merge_access_levels) + protected_branches = + ProtectedBranchesFinder + .new(user_project, params) + .execute + .preload(:push_access_levels, :merge_access_levels) present paginate(protected_branches), with: Entities::ProtectedBranch, project: user_project end diff --git a/lib/api/releases.rb b/lib/api/releases.rb index 506d2b0f985..6e7a99bf0bb 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -67,6 +67,7 @@ module API if result[:status] == :success log_release_created_audit_event(result[:release]) + create_evidence! present result[:release], with: Entities::Release, current_user: current_user else @@ -164,6 +165,16 @@ module API def log_release_milestones_updated_audit_event # This is a separate method so that EE can extend its behaviour end + + def create_evidence! + return if release.historical_release? + + if release.upcoming_release? + CreateEvidenceWorker.perform_at(release.released_at, release.id) # rubocop:disable CodeReuse/Worker + else + CreateEvidenceWorker.perform_async(release.id) # rubocop:disable CodeReuse/Worker + end + end end end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 4106a2cdf38..00473db1ff1 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -103,8 +103,13 @@ module API optional :straight, type: Boolean, desc: 'Comparison method, `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)', default: false end get ':id/repository/compare' do - compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to], straight: params[:straight]) - present compare, with: Entities::Compare + compare = CompareService.new(user_project, params[:to]).execute(user_project, params[:from], straight: params[:straight]) + + if compare + present compare, with: Entities::Compare + else + not_found!("Ref") + end end desc 'Get repository contributors' do diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb index 062115c5103..f7f7c881f4a 100644 --- a/lib/api/resource_label_events.rb +++ b/lib/api/resource_label_events.rb @@ -25,7 +25,7 @@ module API end get ":id/#{eventables_str}/:eventable_id/resource_label_events" do - eventable = find_noteable(parent_type, params[:id], eventable_type, params[:eventable_id]) + eventable = find_noteable(eventable_type, params[:eventable_id]) opts = { page: params[:page], per_page: params[:per_page] } events = ResourceLabelEventFinder.new(current_user, eventable, opts).execute @@ -42,7 +42,8 @@ module API requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable' end get ":id/#{eventables_str}/:eventable_id/resource_label_events/:event_id" do - eventable = find_noteable(parent_type, params[:id], eventable_type, params[:eventable_id]) + eventable = find_noteable(eventable_type, params[:eventable_id]) + event = eventable.resource_label_events.find(params[:event_id]) not_found!('ResourceLabelEvent') unless can?(current_user, :read_resource_label_event, event) diff --git a/lib/api/runner.rb b/lib/api/runner.rb index 60cf9bf2c9c..e1c79aa8efe 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -75,6 +75,13 @@ module API end resource :jobs do + before do + Gitlab::ApplicationContext.push( + user: -> { current_job&.user }, + project: -> { current_job&.project } + ) + end + desc 'Request a job' do success Entities::JobRequest::Response http_codes [[201, 'Job was scheduled'], @@ -276,29 +283,8 @@ module API bad_request!('Missing artifacts file!') unless artifacts file_too_large! unless artifacts.size < max_artifacts_size(job) - expire_in = params['expire_in'] || - Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in - - job.job_artifacts.build( - project: job.project, - file: artifacts, - file_type: params['artifact_type'], - file_format: params['artifact_format'], - file_sha256: artifacts.sha256, - expire_in: expire_in) - - if metadata - job.job_artifacts.build( - project: job.project, - file: metadata, - file_type: :metadata, - file_format: :gzip, - file_sha256: metadata.sha256, - expire_in: expire_in) - end - - if job.update(artifacts_expire_in: expire_in) - present Ci::BuildRunnerPresenter.new(job), with: Entities::JobRequest::Response + if Ci::CreateJobArtifactsService.new.execute(job, artifacts, params, metadata_file: metadata) + status :created else render_validation_error!(job) end diff --git a/lib/api/search.rb b/lib/api/search.rb index 50f930c7c7c..ed52a4fc8f2 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -32,10 +32,6 @@ module API results = SearchService.new(current_user, search_params).search_objects - process_results(results) - end - - def process_results(results) paginate(results) end @@ -47,7 +43,7 @@ module API SCOPE_ENTITY[params[:scope].to_sym] end - def verify_search_scope! + def verify_search_scope!(resource:) # In EE we have additional validation requirements for searches. # Defining this method here as a noop allows us to easily extend it in # EE, without having to modify this file directly. @@ -73,7 +69,7 @@ module API use :pagination end get do - verify_search_scope! + verify_search_scope!(resource: nil) check_users_search_allowed! present search, with: entity @@ -94,7 +90,7 @@ module API use :pagination end get ':id/(-/)search' do - verify_search_scope! + verify_search_scope!(resource: user_group) check_users_search_allowed! present search(group_id: user_group.id), with: entity diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index a7dab373b7f..b5df036c5ca 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -106,7 +106,7 @@ module API snippet = snippets_for_current_user.find_by_id(params.delete(:id)) break not_found!('Snippet') unless snippet - authorize! :update_personal_snippet, snippet + authorize! :update_snippet, snippet attrs = declared_params(include_missing: false).merge(request: request, api: true) service_response = ::Snippets::UpdateService.new(nil, current_user, attrs).execute(snippet) @@ -132,7 +132,7 @@ module API snippet = snippets_for_current_user.find_by_id(params.delete(:id)) break not_found!('Snippet') unless snippet - authorize! :admin_personal_snippet, snippet + authorize! :admin_snippet, snippet destroy_conditionally!(snippet) do |snippet| service = ::Snippets::DestroyService.new(current_user, snippet) diff --git a/lib/api/users.rb b/lib/api/users.rb index bf1fe4fc4a8..c6dc7c08b11 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -52,7 +52,9 @@ module API optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960 optional :avatar, type: File, desc: 'Avatar image for user' # rubocop:disable Scalability/FileUploads - optional :private_profile, type: Boolean, default: false, desc: 'Flag indicating the user has a private profile' + optional :theme_id, type: Integer, default: 1, desc: 'The GitLab theme for the user' + optional :color_scheme_id, type: Integer, default: 1, desc: 'The color scheme for the file viewer' + optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile' all_or_none_of :extern_uid, :provider use :optional_params_ee @@ -223,6 +225,27 @@ module API end # rubocop: enable CodeReuse/ActiveRecord + desc "Delete a user's identity. Available only for admins" do + success Entities::UserWithAdmin + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + requires :provider, type: String, desc: 'The external provider' + end + # rubocop: disable CodeReuse/ActiveRecord + delete ":id/identities/:provider" do + authenticated_as_admin! + + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + identity = user.identities.find_by(provider: params[:provider]) + not_found!('Identity') unless identity + + destroy_conditionally!(identity) + end + # rubocop: enable CodeReuse/ActiveRecord + desc 'Add an SSH key to a specified user. Available only for admins.' do success Entities::SSHKey end @@ -252,17 +275,15 @@ module API success Entities::SSHKey end params do - requires :id, type: Integer, desc: 'The ID of the user' + requires :user_id, type: String, desc: 'The ID or username of the user' use :pagination end - # rubocop: disable CodeReuse/ActiveRecord - get ':id/keys' do - user = User.find_by(id: params[:id]) + get ':user_id/keys', requirements: API::USER_REQUIREMENTS do + user = find_user(params[:user_id]) not_found!('User') unless user && can?(current_user, :read_user, user) present paginate(user.keys), with: Entities::SSHKey end - # rubocop: enable CodeReuse/ActiveRecord desc 'Delete an existing SSH key from a specified user. Available only for admins.' do success Entities::SSHKey @@ -535,6 +556,32 @@ module API end # rubocop: enable CodeReuse/ActiveRecord + desc 'Get memberships' do + success Entities::Membership + end + params do + requires :user_id, type: Integer, desc: 'The ID of the user' + optional :type, type: String, values: %w[Project Namespace] + use :pagination + end + get ":user_id/memberships" do + authenticated_as_admin! + user = find_user_by_id(params) + + members = case params[:type] + when 'Project' + user.project_members + when 'Namespace' + user.group_members + else + user.members + end + + members = members.including_source + + present paginate(members), with: Entities::Membership + end + params do requires :user_id, type: Integer, desc: 'The ID of the user' end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index cb1f2fdcd17..2b6b10cf044 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -135,11 +135,11 @@ module Backup progress.print 'Unpacking backup ... ' - unless Kernel.system(*%W(tar -xf #{tar_file})) + if Kernel.system(*%W(tar -xf #{tar_file})) + progress.puts 'done'.color(:green) + else progress.puts 'unpacking backup failed'.color(:red) exit 1 - else - progress.puts 'done'.color(:green) end ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 974e32ce17c..123a695be13 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -96,6 +96,7 @@ module Backup end wiki = ProjectWiki.new(project) + wiki.repository.remove rescue nil path_to_wiki_bundle = path_to_bundle(wiki) if File.exist?(path_to_wiki_bundle) diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb index 0c1bbd2d250..dec4ec871f1 100644 --- a/lib/banzai/filter/gollum_tags_filter.rb +++ b/lib/banzai/filter/gollum_tags_filter.rb @@ -25,12 +25,10 @@ module Banzai # * [[http://example.com/images/logo.png]] # * [[http://example.com/images/logo.png|alt=Logo]] # - # - Insert a Table of Contents list: - # - # * [[_TOC_]] - # # Based on Gollum::Filter::Tags # + # Note: the table of contents tag is now handled by TableOfContentsTagFilter + # # Context options: # :project_wiki (required) - Current project wiki. # @@ -64,23 +62,11 @@ module Banzai def call doc.search(".//text()").each do |node| next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) + next unless node.content =~ TAGS_PATTERN - # A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running - # before this one, it will be converted into `[[<em>TOC</em>]]`, so it - # needs special-case handling - if toc_tag?(node) - process_toc_tag(node) - else - content = node.content - - next unless content =~ TAGS_PATTERN - - html = process_tag($1) + html = process_tag($1) - if html && html != node.content - node.replace(html) - end - end + node.replace(html) if html && html != node.content end doc @@ -88,12 +74,6 @@ module Banzai private - # Replace an entire `[[<em>TOC</em>]]` node with the result generated by - # TableOfContentsFilter - def process_toc_tag(node) - node.parent.parent.replace(result[:toc].presence || '') - end - # Process a single tag into its final HTML form. # # tag - The String tag contents (the stuff inside the double brackets). @@ -129,12 +109,6 @@ module Banzai end end - def toc_tag?(node) - node.content == 'TOC' && - node.parent.name == 'em' && - node.parent.parent.text == '[[TOC]]' - end - def image?(path) path =~ ALLOWED_IMAGE_EXTENSIONS end diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb index c5a328c21b2..c1f4bf1f97f 100644 --- a/lib/banzai/filter/inline_metrics_filter.rb +++ b/lib/banzai/filter/inline_metrics_filter.rb @@ -25,7 +25,7 @@ module Banzai # Regular expression matching metrics urls def link_pattern - Gitlab::Metrics::Dashboard::Url.regex + Gitlab::Metrics::Dashboard::Url.metrics_regex end private diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb index c70897fccbf..ae830831a27 100644 --- a/lib/banzai/filter/inline_metrics_redactor_filter.rb +++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb @@ -59,7 +59,7 @@ module Banzai embed = Embed.new url = node.attribute('data-dashboard-url').to_s - set_path_and_permission(embed, url, URL.regex, :read_environment) + set_path_and_permission(embed, url, URL.metrics_regex, :read_environment) set_path_and_permission(embed, url, URL.grafana_regex, :read_project) unless embed.permission embeds[node] = embed if embed.permission diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb index 9dfd77b1759..f9d8bf8a1fa 100644 --- a/lib/banzai/filter/issuable_state_filter.rb +++ b/lib/banzai/filter/issuable_state_filter.rb @@ -3,7 +3,7 @@ module Banzai module Filter # HTML filter that appends state information to issuable links. - # Runs as a post-process filter as issuable state might change whilst + # Runs as a post-process filter as issuable state might change while # Markdown is in the cache. # # This filter supports cross-project references. diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index f85be042999..09a4d71b5f6 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -31,3 +31,5 @@ module Banzai end end end + +Banzai::Filter::IssueReferenceFilter.prepend_if_ee('EE::Banzai::Filter::IssueReferenceFilter') diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 4c47ee4dba1..126208db935 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -121,7 +121,7 @@ module Banzai def object_link_text(object, matches) milestone_link = escape_once(super) - reference = object.project&.to_reference(project) + reference = object.project&.to_reference_base(project) if reference.present? "#{milestone_link} <i>in #{reference}</i>".html_safe diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb index 83cf45097ed..292d4b1d56c 100644 --- a/lib/banzai/filter/project_reference_filter.rb +++ b/lib/banzai/filter/project_reference_filter.rb @@ -104,7 +104,7 @@ module Banzai def link_to_project(project, link_content: nil) url = urls.project_url(project, only_path: context[:only_path]) data = data_attribute(project: project.id) - content = link_content || project.to_reference_with_postfix + content = link_content || project.to_reference link_tag(url, data, content, project.name) end diff --git a/lib/banzai/filter/repository_link_filter.rb b/lib/banzai/filter/repository_link_filter.rb index 14cd607cc50..d448238c6e4 100644 --- a/lib/banzai/filter/repository_link_filter.rb +++ b/lib/banzai/filter/repository_link_filter.rb @@ -101,11 +101,18 @@ module Banzai def rebuild_relative_uri(uri) file_path = nested_file_path_if_exists(uri) + resource_type = uri_type(file_path) + + # Repository routes are under /-/ scope now. + # Since we craft a path without using route helpers we must + # ensure - is added here. + prefix = '-' if %w(tree blob raw commits).include?(resource_type.to_s) uri.path = [ relative_url_root, project.full_path, - uri_type(file_path), + prefix, + resource_type, Addressable::URI.escape(ref).gsub('#', '%23'), Addressable::URI.escape(file_path) ].compact.join('/').squeeze('/').chomp('/') diff --git a/lib/banzai/filter/table_of_contents_tag_filter.rb b/lib/banzai/filter/table_of_contents_tag_filter.rb new file mode 100644 index 00000000000..13d0a6a4cc7 --- /dev/null +++ b/lib/banzai/filter/table_of_contents_tag_filter.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # Using `[[_TOC_]]`, inserts a Table of Contents list. + # This syntax is based on the Gollum syntax. This way we have + # some consistency between with wiki and normal markdown. + # If there ever emerges a markdown standard, we can implement + # that here. + # + # The support for this has been removed from GollumTagsFilter + # + # Based on Banzai::Filter::GollumTagsFilter + class TableOfContentsTagFilter < HTML::Pipeline::Filter + TEXT_QUERY = %q(descendant-or-self::text()[ancestor::p and contains(., 'TOC')]) + + def call + return doc if context[:no_header_anchors] + + doc.xpath(TEXT_QUERY).each do |node| + # A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running + # before this one, it will be converted into `[[<em>TOC</em>]]`, so it + # needs special-case handling + process_toc_tag(node) if toc_tag?(node) + end + + doc + end + + private + + # Replace an entire `[[<em>TOC</em>]]` node with the result generated by + # TableOfContentsFilter + def process_toc_tag(node) + node.parent.parent.replace(result[:toc].presence || '') + end + + def toc_tag?(node) + node.content == 'TOC' && + node.parent.name == 'em' && + node.parent.parent.text == '[[TOC]]' + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index f6c12cdb53b..dad0d95685e 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -32,6 +32,7 @@ module Banzai Filter::InlineMetricsFilter, Filter::InlineGrafanaMetricsFilter, Filter::TableOfContentsFilter, + Filter::TableOfContentsTagFilter, Filter::AutolinkFilter, Filter::ExternalLinkFilter, Filter::SuggestionFilter, diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb index 6f6ac08de04..b86c259efbd 100644 --- a/lib/banzai/reference_parser/snippet_parser.rb +++ b/lib/banzai/reference_parser/snippet_parser.rb @@ -12,7 +12,7 @@ module Banzai private def can_read_reference?(user, ref_project, node) - can?(user, :read_project_snippet, referenced_by([node]).first) + can?(user, :read_snippet, referenced_by([node]).first) end end end diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index d41490d2ebd..3e9cf2ab320 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -4,7 +4,7 @@ module Constraints class ProjectUrlConstrainer def matches?(request, existence_check: true) namespace_path = request.params[:namespace_id] - project_path = request.params[:project_id] || request.params[:id] + project_path = request.params[:project_id] || request.params[:id] || request.params[:repository_id] full_path = [namespace_path, project_path].join('/') return false unless ProjectPathValidator.valid_path?(full_path) diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index bc0347f6ea1..12f7f04634f 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -6,6 +6,8 @@ require 'digest' module ContainerRegistry class Client + include Gitlab::Utils::StrongMemoize + attr_accessor :uri DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json' @@ -35,10 +37,25 @@ module ContainerRegistry response.headers['docker-content-digest'] if response.success? end - def delete_repository_tag(name, reference) - result = faraday.delete("/v2/#{name}/manifests/#{reference}") + def delete_repository_tag_by_digest(name, reference) + delete_if_exists("/v2/#{name}/manifests/#{reference}") + end - result.success? || result.status == 404 + def delete_repository_tag_by_name(name, reference) + delete_if_exists("/v2/#{name}/tags/reference/#{reference}") + end + + # Check if the registry supports tag deletion. This is only supported by the + # GitLab registry fork. The fastest and safest way to check this is to send + # an OPTIONS request to /v2/<name>/tags/reference/<tag>, using a random + # repository name and tag (the registry won't check if they exist). + # Registries that support tag deletion will reply with a 200 OK and include + # the DELETE method in the Allow header. Others reply with an 404 Not Found. + def supports_tag_delete? + strong_memoize(:supports_tag_delete) do + response = faraday.run_request(:options, '/v2/name/tags/reference/tag', '', {}) + response.success? && response.headers['allow']&.include?('DELETE') + end end def upload_raw_blob(path, blob) @@ -86,9 +103,7 @@ module ContainerRegistry end def delete_blob(name, digest) - result = faraday.delete("/v2/#{name}/blobs/#{digest}") - - result.success? || result.status == 404 + delete_if_exists("/v2/#{name}/blobs/#{digest}") end def put_tag(name, reference, manifest) @@ -163,6 +178,12 @@ module ContainerRegistry conn.adapter :net_http end end + + def delete_if_exists(path) + result = faraday.delete(path) + + result.success? || result.status == 404 + end end end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index 3c308258a3f..e1a2891e43a 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -118,7 +118,7 @@ module ContainerRegistry def unsafe_delete return unless digest - client.delete_repository_tag(repository.path, digest) + client.delete_repository_tag_by_digest(repository.path, digest) end end end diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb index f739fe5e16e..70666dc7924 100644 --- a/lib/declarative_policy/runner.rb +++ b/lib/declarative_policy/runner.rb @@ -33,6 +33,7 @@ module DeclarativePolicy attr_reader :steps def initialize(steps) @steps = steps + @state = nil end # We make sure only to run any given Runner once, diff --git a/lib/feature.rb b/lib/feature.rb index 543512b1598..aadc2c64957 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -32,6 +32,8 @@ class Feature end def persisted_names + return [] unless Gitlab::Database.exists? + Gitlab::SafeRequestStore[:flipper_persisted_names] ||= begin # We saw on GitLab.com, this database request was called 2300 diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb index 2bd55c36a03..d327162b34e 100644 --- a/lib/feature/gitaly.rb +++ b/lib/feature/gitaly.rb @@ -1,33 +1,25 @@ # frozen_string_literal: true -require 'set' - class Feature class Gitaly - # Server feature flags should use '_' to separate words. - SERVER_FEATURE_FLAGS = - %w[ - cache_invalidator - inforef_uploadpack_cache - get_tag_messages_go - filter_shas_with_signatures_go - ].freeze - - DEFAULT_ON_FLAGS = Set.new([]).freeze + PREFIX = "gitaly_" class << self def enabled?(feature_flag) return false unless Feature::FlipperFeature.table_exists? - default_on = DEFAULT_ON_FLAGS.include?(feature_flag) - Feature.enabled?("gitaly_#{feature_flag}", default_enabled: default_on) + Feature.enabled?("#{PREFIX}#{feature_flag}") rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad false end def server_feature_flags - SERVER_FEATURE_FLAGS.map do |f| - ["gitaly-feature-#{f.tr('_', '-')}", enabled?(f).to_s] + Feature.persisted_names + .select { |f| f.start_with?(PREFIX) } + .map do |f| + flag = f.delete_prefix(PREFIX) + + ["gitaly-feature-#{flag.tr('_', '-')}", enabled?(flag).to_s] end.to_h end end diff --git a/lib/gitaly/server.rb b/lib/gitaly/server.rb index 64ab5db4fcd..89a836e629f 100644 --- a/lib/gitaly/server.rb +++ b/lib/gitaly/server.rb @@ -53,6 +53,20 @@ module Gitaly storage_status&.fs_type end + def disk_used + disk_statistics_storage_status&.used + end + + def disk_available + disk_statistics_storage_status&.available + end + + # Simple convenience method for when obtaining both used and available + # statistics at once is preferred. + def disk_stats + disk_statistics_storage_status + end + def address Gitlab::GitalyClient.address(@storage) rescue RuntimeError => e @@ -65,6 +79,10 @@ module Gitaly @storage_status ||= info.storage_statuses.find { |s| s.storage_name == storage } end + def disk_statistics_storage_status + @disk_statistics_storage_status ||= disk_statistics.storage_statuses.find { |s| s.storage_name == storage } + end + def matches_sha? match = server_version.match(SHA_VERSION_REGEX) return false unless match @@ -76,7 +94,19 @@ module Gitaly @info ||= begin Gitlab::GitalyClient::ServerService.new(@storage).info - rescue GRPC::Unavailable, GRPC::DeadlineExceeded + rescue GRPC::Unavailable, GRPC::DeadlineExceeded => ex + Gitlab::ErrorTracking.track_exception(ex) + # This will show the server as being out of date + Gitaly::ServerInfoResponse.new(git_version: '', server_version: '', storage_statuses: []) + end + end + + def disk_statistics + @disk_statistics ||= + begin + Gitlab::GitalyClient::ServerService.new(@storage).disk_statistics + rescue GRPC::Unavailable, GRPC::DeadlineExceeded => ex + Gitlab::ErrorTracking.track_exception(ex) # This will show the server as being out of date Gitaly::ServerInfoResponse.new(git_version: '', server_version: '', storage_statuses: []) end diff --git a/lib/gitlab/action_view_output/context.rb b/lib/gitlab/action_view_output/context.rb deleted file mode 100644 index 9fbc9811636..00000000000 --- a/lib/gitlab/action_view_output/context.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -# This file was simplified from https://raw.githubusercontent.com/rails/rails/195f39804a7a4a0034f25e8704220e03d95a752a/actionview/lib/action_view/context.rb. -# -# It is only needed by modules that need to call ActionView helper -# methods (e.g. those in -# https://github.com/rails/rails/tree/c4d3e202e10ae627b3b9c34498afb45450652421/actionview/lib/action_view/helpers) -# to generate tags outside of a Rails controller (e.g. API, Sidekiq, -# etc.). -# -# In Rails 5, ActionView::Context automatically includes CompiledTemplates. -# This means that any module that includes ActionView::Context is now a descendant -# of CompiledTemplates. -# -# When a partial is rendered for the first time, it runs -# Module#module_eval, which will evaluate a string source that defines a -# new method. For example: -# -# def _app_views_profiles_show_html_haml___1285955918103175884_70307801785400(local_assigns, output_buffer) -# "hello world" -# end -# -# When a new method is defined, the Ruby interpreter clears the method -# cache for all descendants, and all methods for those modules will have -# to be redefined. This can lead to a significant performance penalty. -# -# Rails 6 fixes this behavior by moving out the `include -# CompiledTemplates` into ActionView::Base so that including `ActionView::Context` -# doesn't quietly affect other modules in this way. - -if Rails::VERSION::STRING.start_with?('6') - raise 'This module is no longer needed in Rails 6. Use ActionView::Context instead.' -end - -module Gitlab - module ActionViewOutput - module Context - attr_accessor :output_buffer, :view_flow - end - end -end diff --git a/lib/gitlab/alerting/alert.rb b/lib/gitlab/alerting/alert.rb new file mode 100644 index 00000000000..531307b93d4 --- /dev/null +++ b/lib/gitlab/alerting/alert.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +module Gitlab + module Alerting + class Alert + include ActiveModel::Model + include Gitlab::Utils::StrongMemoize + include Presentable + + attr_accessor :project, :payload + + def gitlab_alert + strong_memoize(:gitlab_alert) do + parse_gitlab_alert_from_payload + end + end + + def metric_id + strong_memoize(:metric_id) do + payload&.dig('labels', 'gitlab_alert_id') + end + end + + def title + strong_memoize(:title) do + gitlab_alert&.title || parse_title_from_payload + end + end + + def description + strong_memoize(:description) do + parse_description_from_payload + end + end + + def environment + strong_memoize(:environment) do + gitlab_alert&.environment || parse_environment_from_payload + end + end + + def annotations + strong_memoize(:annotations) do + parse_annotations_from_payload || [] + end + end + + def starts_at + strong_memoize(:starts_at) do + parse_datetime_from_payload('startsAt') + end + end + + def starts_at_raw + strong_memoize(:starts_at_raw) do + payload&.dig('startsAt') + end + end + + def ends_at + strong_memoize(:ends_at) do + parse_datetime_from_payload('endsAt') + end + end + + def full_query + strong_memoize(:full_query) do + gitlab_alert&.full_query || parse_expr_from_payload + end + end + + def alert_markdown + strong_memoize(:alert_markdown) do + parse_alert_markdown_from_payload + end + end + + def status + strong_memoize(:status) do + payload&.dig('status') + end + end + + def firing? + status == 'firing' + end + + def resolved? + status == 'resolved' + end + + def gitlab_managed? + metric_id.present? + end + + def valid? + payload.respond_to?(:dig) && project && title && starts_at + end + + def present + super(presenter_class: Projects::Prometheus::AlertPresenter) + end + + private + + def parse_environment_from_payload + environment_name = payload&.dig('labels', 'gitlab_environment_name') + + return unless environment_name + + EnvironmentsFinder.new(project, nil, { name: environment_name }) + .find + &.first + end + + def parse_gitlab_alert_from_payload + return unless metric_id + + Projects::Prometheus::AlertsFinder + .new(project: project, metric: metric_id) + .execute + .first + end + + def parse_title_from_payload + payload&.dig('annotations', 'title') || + payload&.dig('annotations', 'summary') || + payload&.dig('labels', 'alertname') + end + + def parse_description_from_payload + payload&.dig('annotations', 'description') + end + + def parse_annotations_from_payload + payload&.dig('annotations')&.map do |label, value| + Alerting::AlertAnnotation.new(label: label, value: value) + end + end + + def parse_datetime_from_payload(field) + value = payload&.dig(field) + return unless value + + Time.rfc3339(value) + rescue ArgumentError + end + + # Parses `g0.expr` from `generatorURL`. + # + # Example: http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1 + def parse_expr_from_payload + url = payload&.dig('generatorURL') + return unless url + + uri = URI(url) + + Rack::Utils.parse_query(uri.query).fetch('g0.expr') + rescue URI::InvalidURIError, KeyError + end + + def parse_alert_markdown_from_payload + payload&.dig('annotations', 'gitlab_incident_markdown') + end + end + end +end diff --git a/lib/gitlab/alerting/alert_annotation.rb b/lib/gitlab/alerting/alert_annotation.rb new file mode 100644 index 00000000000..a4b3a97b08c --- /dev/null +++ b/lib/gitlab/alerting/alert_annotation.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Gitlab + module Alerting + class AlertAnnotation + include ActiveModel::Model + + attr_accessor :label, :value + end + end +end diff --git a/lib/gitlab/alerting/notification_payload_parser.rb b/lib/gitlab/alerting/notification_payload_parser.rb new file mode 100644 index 00000000000..a54bb44d66a --- /dev/null +++ b/lib/gitlab/alerting/notification_payload_parser.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Gitlab + module Alerting + class NotificationPayloadParser + BadPayloadError = Class.new(StandardError) + + DEFAULT_TITLE = 'New: Incident' + + def initialize(payload) + @payload = payload.to_h.with_indifferent_access + end + + def self.call(payload) + new(payload).call + end + + def call + { + 'annotations' => annotations, + 'startsAt' => starts_at + }.compact + end + + private + + attr_reader :payload + + def title + payload[:title].presence || DEFAULT_TITLE + end + + def annotations + primary_params + .reverse_merge(flatten_secondary_params) + .transform_values(&:presence) + .compact + end + + def primary_params + { + 'title' => title, + 'description' => payload[:description], + 'monitoring_tool' => payload[:monitoring_tool], + 'service' => payload[:service], + 'hosts' => hosts.presence + } + end + + def hosts + Array(payload[:hosts]).reject(&:blank?) + end + + def current_time + Time.current.change(usec: 0).rfc3339 + end + + def starts_at + Time.parse(payload[:start_time].to_s).rfc3339 + rescue ArgumentError + current_time + end + + def secondary_params + payload.except(:start_time) + end + + def flatten_secondary_params + Gitlab::Utils::SafeInlineHash.merge_keys!(secondary_params) + rescue ArgumentError + raise BadPayloadError, 'The payload is too big' + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/default_stages.rb b/lib/gitlab/analytics/cycle_analytics/default_stages.rb index 8e70236ce75..79e60e28fc7 100644 --- a/lib/gitlab/analytics/cycle_analytics/default_stages.rb +++ b/lib/gitlab/analytics/cycle_analytics/default_stages.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# This module represents the default Cycle Analytics stages that are currently provided by CE +# This module represents the default Value Stream Analytics stages that are currently provided by CE # Each method returns a hash that can be used to build a new stage object. # # Example: diff --git a/lib/gitlab/application_context.rb b/lib/gitlab/application_context.rb index 71dbfea70e8..b950bfb0f3a 100644 --- a/lib/gitlab/application_context.rb +++ b/lib/gitlab/application_context.rb @@ -5,17 +5,18 @@ module Gitlab class ApplicationContext include Gitlab::Utils::LazyAttributes - Attribute = Struct.new(:name, :type) + Attribute = Struct.new(:name, :type, :evaluation) APPLICATION_ATTRIBUTES = [ Attribute.new(:project, Project), Attribute.new(:namespace, Namespace), - Attribute.new(:user, User) + Attribute.new(:user, User), + Attribute.new(:caller_id, String) ].freeze def self.with_context(args, &block) application_context = new(**args) - Labkit::Context.with_context(application_context.to_lazy_hash, &block) + application_context.use(&block) end def self.push(args) @@ -37,9 +38,14 @@ module Gitlab hash[:user] = -> { username } if set_values.include?(:user) hash[:project] = -> { project_path } if set_values.include?(:project) hash[:root_namespace] = -> { root_namespace_path } if include_namespace? + hash[:caller_id] = caller_id if set_values.include?(:caller_id) end end + def use + Labkit::Context.with_context(to_lazy_hash) { yield } + end + private attr_reader :set_values @@ -75,3 +81,5 @@ module Gitlab end end end + +Gitlab::ApplicationContext.prepend_if_ee('EE::Gitlab::ApplicationContext') diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb index 629632b744b..49e1f1edfb9 100644 --- a/lib/gitlab/application_rate_limiter.rb +++ b/lib/gitlab/application_rate_limiter.rb @@ -22,6 +22,7 @@ module Gitlab project_export: { threshold: 1, interval: 5.minutes }, project_download_export: { threshold: 10, interval: 10.minutes }, project_generate_new_export: { threshold: 1, interval: 5.minutes }, + project_import: { threshold: 30, interval: 10.minutes }, play_pipeline_schedule: { threshold: 1, interval: 1.minute }, show_raw_controller: { threshold: -> { Gitlab::CurrentSettings.current_application_settings.raw_blob_request_limit }, interval: 1.minute } }.freeze diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 821c68dbedc..1329357d0b8 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -49,7 +49,7 @@ module Gitlab lfs_token_check(login, password, project) || oauth_access_token_check(login, password) || personal_access_token_check(password) || - deploy_token_check(login, password) || + deploy_token_check(login, password, project) || user_with_password_for_git(login, password) || Gitlab::Auth::Result.new @@ -208,7 +208,7 @@ module Gitlab end.uniq end - def deploy_token_check(login, password) + def deploy_token_check(login, password, project) return unless password.present? token = DeployToken.active.find_by_token(password) @@ -219,7 +219,7 @@ module Gitlab scopes = abilities_for_scopes(token.scopes) if valid_scoped_token?(token, all_available_scopes) - Gitlab::Auth::Result.new(token, token.project, :deploy_token, scopes) + Gitlab::Auth::Result.new(token, project, :deploy_token, scopes) end end diff --git a/lib/gitlab/auth/current_user_mode.rb b/lib/gitlab/auth/current_user_mode.rb index cb39baaa6cc..1ef95c03cfc 100644 --- a/lib/gitlab/auth/current_user_mode.rb +++ b/lib/gitlab/auth/current_user_mode.rb @@ -10,12 +10,54 @@ module Gitlab class CurrentUserMode NotRequestedError = Class.new(StandardError) + # RequestStore entries + CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY = { res: :current_user_mode, data: :bypass_session_admin_id }.freeze + CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY = { res: :current_user_mode, data: :current_admin }.freeze + + # SessionStore entries SESSION_STORE_KEY = :current_user_mode - ADMIN_MODE_START_TIME_KEY = 'admin_mode' - ADMIN_MODE_REQUESTED_TIME_KEY = 'admin_mode_requested' + ADMIN_MODE_START_TIME_KEY = :admin_mode + ADMIN_MODE_REQUESTED_TIME_KEY = :admin_mode_requested MAX_ADMIN_MODE_TIME = 6.hours ADMIN_MODE_REQUESTED_GRACE_PERIOD = 5.minutes + class << self + # Admin mode activation requires storing a flag in the user session. Using this + # method when scheduling jobs in Sidekiq will bypass the session check for a + # user that was already in admin mode + def bypass_session!(admin_id) + Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY] = admin_id + + Gitlab::AppLogger.debug("Bypassing session in admin mode for: #{admin_id}") + + yield + ensure + Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY) + end + + def bypass_session_admin_id + Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY] + end + + # Store in the current request the provided user model (only if in admin mode) + # and yield + def with_current_admin(admin) + return yield unless self.new(admin).admin_mode? + + Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY] = admin + + Gitlab::AppLogger.debug("Admin mode active for: #{admin.username}") + + yield + ensure + Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY) + end + + def current_admin + Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY] + end + end + def initialize(user) @user = user end @@ -42,7 +84,7 @@ module Gitlab raise NotRequestedError unless admin_mode_requested? - reset_request_store + reset_request_store_cache_entries current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now @@ -55,7 +97,7 @@ module Gitlab def disable_admin_mode! return unless user&.admin? - reset_request_store + reset_request_store_cache_entries current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil current_session_data[ADMIN_MODE_START_TIME_KEY] = nil @@ -64,7 +106,7 @@ module Gitlab def request_admin_mode! return unless user&.admin? - reset_request_store + reset_request_store_cache_entries current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = Time.now end @@ -73,10 +115,12 @@ module Gitlab attr_reader :user + # RequestStore entry to cache #admin_mode? result def admin_mode_rs_key @admin_mode_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode? } end + # RequestStore entry to cache #admin_mode_requested? result def admin_mode_requested_rs_key @admin_mode_requested_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode_requested? } end @@ -86,6 +130,7 @@ module Gitlab end def any_session_with_admin_mode? + return true if bypass_session? return true if current_session_data.initiated? && current_session_data[ADMIN_MODE_START_TIME_KEY].to_i > MAX_ADMIN_MODE_TIME.ago.to_i all_sessions.any? do |session| @@ -103,7 +148,11 @@ module Gitlab current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY].to_i > ADMIN_MODE_REQUESTED_GRACE_PERIOD.ago.to_i end - def reset_request_store + def bypass_session? + user&.id && user.id == self.class.bypass_session_admin_id + end + + def reset_request_store_cache_entries Gitlab::SafeRequestStore.delete(admin_mode_rs_key) Gitlab::SafeRequestStore.delete(admin_mode_requested_rs_key) end diff --git a/lib/gitlab/auth/ldap/access.rb b/lib/gitlab/auth/ldap/access.rb index b8ed740e08c..940b802be7e 100644 --- a/lib/gitlab/auth/ldap/access.rb +++ b/lib/gitlab/auth/ldap/access.rb @@ -49,7 +49,7 @@ module Gitlab return true end - # Block user in GitLab if he/she was blocked in AD + # Block user in GitLab if they were blocked in AD if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(ldap_identity.extern_uid, adapter) block_user(user, 'is disabled in Active Directory') false diff --git a/lib/gitlab/auth/saml/auth_hash.rb b/lib/gitlab/auth/saml/auth_hash.rb index b0df9757bbd..a2b0dfd5c66 100644 --- a/lib/gitlab/auth/saml/auth_hash.rb +++ b/lib/gitlab/auth/saml/auth_hash.rb @@ -28,7 +28,7 @@ module Gitlab end def extract_authn_context(document) - REXML::XPath.first(document, "//*[name()='saml:AuthnStatement' or name()='saml2:AuthnStatement']/*[name()='saml:AuthnContext' or name()='saml2:AuthnContext']/*[name()='saml:AuthnContextClassRef' or name()='saml2:AuthnContextClassRef']/text()").to_s + REXML::XPath.first(document, "//*[name()='saml:AuthnStatement' or name()='saml2:AuthnStatement' or name()='AuthnStatement']/*[name()='saml:AuthnContext' or name()='saml2:AuthnContext' or name()='AuthnContext']/*[name()='saml:AuthnContextClassRef' or name()='saml2:AuthnContextClassRef' or name()='AuthnContextClassRef']/text()").to_s end end end diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb index ddd6b11eebb..6a16c37e880 100644 --- a/lib/gitlab/background_migration.rb +++ b/lib/gitlab/background_migration.rb @@ -58,6 +58,14 @@ module Gitlab migration_class_for(class_name).new.perform(*arguments) end + def self.remaining + scheduled = Sidekiq::ScheduledSet.new.count do |job| + job.queue == self.queue + end + + scheduled + Sidekiq::Queue.new(self.queue).size + end + def self.exists?(migration_class, additional_queues = []) enqueued = Sidekiq::Queue.new(self.queue) scheduled = Sidekiq::ScheduledSet.new diff --git a/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications.rb b/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications.rb deleted file mode 100644 index 19f5821d449..00000000000 --- a/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module BackgroundMigration - # Create missing PrometheusServices records or sets active attribute to true - # for all projects which belongs to cluster with Prometheus Application installed. - class ActivatePrometheusServicesForSharedClusterApplications - module Migratable - # Migration model namespace isolated from application code. - class PrometheusService < ActiveRecord::Base - self.inheritance_column = :_type_disabled - self.table_name = 'services' - - default_scope { where("services.type = 'PrometheusService'") } - - def self.for_project(project_id) - new( - project_id: project_id, - active: true, - properties: '{}', - type: 'PrometheusService', - template: false, - push_events: true, - issues_events: true, - merge_requests_events: true, - tag_push_events: true, - note_events: true, - category: 'monitoring', - default: false, - wiki_page_events: true, - pipeline_events: true, - confidential_issues_events: true, - commit_events: true, - job_events: true, - confidential_note_events: true, - deployment_events: false - ) - end - - def managed? - properties == '{}' - end - end - end - - def perform(project_id) - service = Migratable::PrometheusService.find_by(project_id: project_id) || Migratable::PrometheusService.for_project(project_id) - service.update!(active: true) if service.managed? - end - end - end -end diff --git a/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb b/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb index 3c142327e94..2a079060380 100644 --- a/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb +++ b/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb @@ -11,7 +11,7 @@ module Gitlab module Storage # Class that returns the disk path for a project using hashed storage - class HashedProject + class Hashed attr_accessor :project ROOT_PATH_PREFIX = '@hashed' @@ -121,7 +121,7 @@ module Gitlab def storage @storage ||= if hashed_storage? - Storage::HashedProject.new(self) + Storage::Hashed.new(self) else Storage::LegacyProject.new(self) end diff --git a/lib/gitlab/background_migration/backfill_project_repositories.rb b/lib/gitlab/background_migration/backfill_project_repositories.rb index 1d9aa050041..263546bd132 100644 --- a/lib/gitlab/background_migration/backfill_project_repositories.rb +++ b/lib/gitlab/background_migration/backfill_project_repositories.rb @@ -46,7 +46,7 @@ module Gitlab module Storage # Class that returns the disk path for a project using hashed storage - class HashedProject + class Hashed attr_accessor :project ROOT_PATH_PREFIX = '@hashed' @@ -176,7 +176,7 @@ module Gitlab def storage @storage ||= if hashed_storage? - Storage::HashedProject.new(self) + Storage::Hashed.new(self) else Storage::LegacyProject.new(self) end diff --git a/lib/gitlab/background_migration/backfill_project_settings.rb b/lib/gitlab/background_migration/backfill_project_settings.rb new file mode 100644 index 00000000000..7d7f19e1fda --- /dev/null +++ b/lib/gitlab/background_migration/backfill_project_settings.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # Backfill project_settings for a range of projects + class BackfillProjectSettings + def perform(start_id, end_id) + ActiveRecord::Base.connection.execute <<~SQL + INSERT INTO project_settings (project_id, created_at, updated_at) + SELECT projects.id, now(), now() + FROM projects + WHERE projects.id BETWEEN #{start_id} AND #{end_id} + ON CONFLICT (project_id) DO NOTHING; + SQL + end + end + end +end diff --git a/lib/gitlab/background_migration/fix_orphan_promoted_issues.rb b/lib/gitlab/background_migration/fix_orphan_promoted_issues.rb new file mode 100644 index 00000000000..d6ec56ae19e --- /dev/null +++ b/lib/gitlab/background_migration/fix_orphan_promoted_issues.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # No OP for CE + class FixOrphanPromotedIssues + def perform(note_id) + end + end + end +end + +Gitlab::BackgroundMigration::FixOrphanPromotedIssues.prepend_if_ee('EE::Gitlab::BackgroundMigration::FixOrphanPromotedIssues') diff --git a/lib/gitlab/background_migration/fix_projects_without_project_feature.rb b/lib/gitlab/background_migration/fix_projects_without_project_feature.rb new file mode 100644 index 00000000000..68665db522e --- /dev/null +++ b/lib/gitlab/background_migration/fix_projects_without_project_feature.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This migration creates missing project_features records + # for the projects within the given range of ids + class FixProjectsWithoutProjectFeature + def perform(from_id, to_id) + if number_of_created_records = create_missing!(from_id, to_id) > 0 + log(number_of_created_records, from_id, to_id) + end + end + + private + + def create_missing!(from_id, to_id) + result = ActiveRecord::Base.connection.select_one(sql(from_id, to_id)) + return 0 unless result + + result['number_of_created_records'] + end + + def sql(from_id, to_id) + <<~SQL + WITH created_records AS ( + INSERT INTO project_features ( + project_id, + merge_requests_access_level, + issues_access_level, + wiki_access_level, + snippets_access_level, + builds_access_level, + repository_access_level, + forking_access_level, + pages_access_level, + created_at, + updated_at + ) + SELECT projects.id, + 20, 20, 20, 20, 20, 20, 20, + #{pages_access_level}, + TIMEZONE('UTC', NOW()), TIMEZONE('UTC', NOW()) + FROM projects + WHERE projects.id BETWEEN #{Integer(from_id)} AND #{Integer(to_id)} + AND NOT EXISTS ( + SELECT 1 FROM project_features + WHERE project_features.project_id = projects.id + ) + ON CONFLICT (project_id) DO NOTHING + RETURNING * + ) + SELECT COUNT(*) as number_of_created_records + FROM created_records + SQL + end + + def pages_access_level + if ::Gitlab::Pages.access_control_is_forced? + "10" + else + "GREATEST(projects.visibility_level, 10)" + end + end + + def log(count, from_id, to_id) + logger = Gitlab::BackgroundMigration::Logger.build + + logger.info(message: "FixProjectsWithoutProjectFeature: created missing project_features for #{count} projects in id=#{from_id}...#{to_id}") + end + end + end +end diff --git a/lib/gitlab/background_migration/link_lfs_objects.rb b/lib/gitlab/background_migration/link_lfs_objects.rb new file mode 100644 index 00000000000..69c03f617bf --- /dev/null +++ b/lib/gitlab/background_migration/link_lfs_objects.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # Create missing LfsObjectsProject records for forks + class LinkLfsObjects + # Model definition used for migration + class ForkNetworkMember < ActiveRecord::Base + self.table_name = 'fork_network_members' + + def self.with_non_existing_lfs_objects + joins('JOIN lfs_objects_projects lop ON fork_network_members.forked_from_project_id = lop.project_id') + .where( + <<~SQL + NOT EXISTS ( + SELECT 1 + FROM lfs_objects_projects + WHERE lfs_objects_projects.project_id = fork_network_members.project_id + AND lfs_objects_projects.lfs_object_id = lop.lfs_object_id + ) + SQL + ) + end + end + + def perform(start_id, end_id) + # no-op as some queries times out + end + end + end +end diff --git a/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent.rb b/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent.rb new file mode 100644 index 00000000000..9e330f7c008 --- /dev/null +++ b/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This background migration updates children of group to match visibility of a parent + class UpdateExistingSubgroupToMatchVisibilityLevelOfParent + def perform(parents_groups_ids, level) + groups_ids = Gitlab::ObjectHierarchy.new(Group.where(id: parents_groups_ids)) + .base_and_descendants + .where("visibility_level > ?", level) + .select(:id) + + return if groups_ids.empty? + + Group + .where(id: groups_ids) + .update_all(visibility_level: level) + end + end + end +end diff --git a/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb b/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb new file mode 100644 index 00000000000..40f45301727 --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + module UserMentions + class CreateResourceUserMention + # Resources that have mentions to be migrated: + # issue, merge_request, epic, commit, snippet, design + + BULK_INSERT_SIZE = 5000 + ISOLATION_MODULE = 'Gitlab::BackgroundMigration::UserMentions::Models' + + def perform(resource_model, join, conditions, with_notes, start_id, end_id) + resource_model = "#{ISOLATION_MODULE}::#{resource_model}".constantize if resource_model.is_a?(String) + model = with_notes ? Gitlab::BackgroundMigration::UserMentions::Models::Note : resource_model + resource_user_mention_model = resource_model.user_mention_model + + records = model.joins(join).where(conditions).where(id: start_id..end_id) + + records.in_groups_of(BULK_INSERT_SIZE, false).each do |records| + mentions = [] + records.each do |record| + mentions << record.build_mention_values(resource_user_mention_model.resource_foreign_key) + end + + Gitlab::Database.bulk_insert( + resource_user_mention_model.table_name, + mentions, + return_ids: true, + disable_quote: resource_model.no_quote_columns, + on_conflict: :do_nothing + ) + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb b/lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb new file mode 100644 index 00000000000..b7fa92a6686 --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/models/concerns/isolated_mentionable.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module UserMentions + module Models + # == IsolatedMentionable concern + # + # Shortcutted for isolation version of Mentionable to be used in mentions migrations + # + module IsolatedMentionable + extend ::ActiveSupport::Concern + + class_methods do + # Indicate which attributes of the Mentionable to search for GFM references. + def attr_mentionable(attr, options = {}) + attr = attr.to_s + mentionable_attrs << [attr, options] + end + end + + included do + # Accessor for attributes marked mentionable. + cattr_accessor :mentionable_attrs, instance_accessor: false do + [] + end + + if self < Participable + participant -> (user, ext) { all_references(user, extractor: ext) } + end + end + + def all_references(current_user = nil, extractor: nil) + # Use custom extractor if it's passed in the function parameters. + if extractor + extractors[current_user] = extractor + else + extractor = extractors[current_user] ||= ::Gitlab::ReferenceExtractor.new(project, current_user) + + extractor.reset_memoized_values + end + + self.class.mentionable_attrs.each do |attr, options| + text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend + options = options.merge( + cache_key: [self, attr], + author: author, + skip_project_check: skip_project_check? + ).merge(mentionable_params) + + cached_html = self.try(:updated_cached_html_for, attr.to_sym) + options[:rendered] = cached_html if cached_html + + extractor.analyze(text, options) + end + + extractor + end + + def extractors + @extractors ||= {} + end + + def skip_project_check? + false + end + + def build_mention_values(resource_foreign_key) + refs = all_references(author) + + { + "#{resource_foreign_key}": user_mention_resource_id, + note_id: user_mention_note_id, + mentioned_users_ids: array_to_sql(refs.mentioned_users.pluck(:id)), + mentioned_projects_ids: array_to_sql(refs.mentioned_projects.pluck(:id)), + mentioned_groups_ids: array_to_sql(refs.mentioned_groups.pluck(:id)) + } + end + + def array_to_sql(ids_array) + return unless ids_array.present? + + '{' + ids_array.join(", ") + '}' + end + + private + + def mentionable_params + {} + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/user_mentions/models/concerns/mentionable_migration_methods.rb b/lib/gitlab/background_migration/user_mentions/models/concerns/mentionable_migration_methods.rb new file mode 100644 index 00000000000..fa479cb0ed3 --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/models/concerns/mentionable_migration_methods.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module UserMentions + module Models + # Extract common no_quote_columns method used in determining the columns that do not need + # to be quoted for corresponding models + module MentionableMigrationMethods + extend ::ActiveSupport::Concern + + class_methods do + def no_quote_columns + [ + :note_id, + user_mention_model.resource_foreign_key + ] + end + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/user_mentions/models/epic.rb b/lib/gitlab/background_migration/user_mentions/models/epic.rb new file mode 100644 index 00000000000..9797c86478e --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/models/epic.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + module UserMentions + module Models + class Epic < ActiveRecord::Base + include IsolatedMentionable + include CacheMarkdownField + include MentionableMigrationMethods + + attr_mentionable :title, pipeline: :single_line + attr_mentionable :description + cache_markdown_field :title, pipeline: :single_line + cache_markdown_field :description, issuable_state_filter_enabled: true + + self.table_name = 'epics' + + belongs_to :author, class_name: "User" + belongs_to :project + belongs_to :group + + def self.user_mention_model + Gitlab::BackgroundMigration::UserMentions::Models::EpicUserMention + end + + def user_mention_model + self.class.user_mention_model + end + + def project + nil + end + + def mentionable_params + { group: group, label_url_method: :group_epics_url } + end + + def user_mention_resource_id + id + end + + def user_mention_note_id + 'NULL' + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/user_mentions/models/epic_user_mention.rb b/lib/gitlab/background_migration/user_mentions/models/epic_user_mention.rb new file mode 100644 index 00000000000..4e3ce9bf3a7 --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/models/epic_user_mention.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + module UserMentions + module Models + class EpicUserMention < ActiveRecord::Base + self.table_name = 'epic_user_mentions' + + def self.resource_foreign_key + :epic_id + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/user_mentions/models/note.rb b/lib/gitlab/background_migration/user_mentions/models/note.rb new file mode 100644 index 00000000000..dc364d7af5a --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/models/note.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + module UserMentions + module Models + class Note < ActiveRecord::Base + include IsolatedMentionable + include CacheMarkdownField + + self.table_name = 'notes' + self.inheritance_column = :_type_disabled + + attr_mentionable :note, pipeline: :note + cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true + + belongs_to :author, class_name: "User" + belongs_to :noteable, polymorphic: true + belongs_to :project + + def for_personal_snippet? + noteable.class.name == 'PersonalSnippet' + end + + def for_project_noteable? + !for_personal_snippet? + end + + def skip_project_check? + !for_project_noteable? + end + + def for_epic? + noteable.class.name == 'Epic' + end + + def user_mention_resource_id + noteable_id || commit_id + end + + def user_mention_note_id + id + end + + private + + def mentionable_params + return super unless for_epic? + + super.merge(banzai_context_params) + end + + def banzai_context_params + { group: noteable.group, label_url_method: :group_epics_url } + end + end + end + end + end +end diff --git a/lib/gitlab/batch_worker_context.rb b/lib/gitlab/batch_worker_context.rb new file mode 100644 index 00000000000..0589206fefc --- /dev/null +++ b/lib/gitlab/batch_worker_context.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + class BatchWorkerContext + def initialize(objects, arguments_proc:, context_proc:) + @objects = objects + @arguments_proc = arguments_proc + @context_proc = context_proc + end + + def arguments + context_by_arguments.keys + end + + def context_for(arguments) + context_by_arguments[arguments] + end + + private + + attr_reader :objects, :arguments_proc, :context_proc + + def context_by_arguments + @context_by_arguments ||= objects.each_with_object({}) do |object, result| + arguments = Array.wrap(arguments_proc.call(object)) + context = Gitlab::ApplicationContext.new(context_proc.call(object)) + + result[arguments] = context + end + end + end +end diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb index ea7013db2ce..e7a7d23ef7e 100644 --- a/lib/gitlab/cache/ci/project_pipeline_status.rb +++ b/lib/gitlab/cache/ci/project_pipeline_status.rb @@ -59,6 +59,10 @@ module Gitlab end self.loaded = true + rescue GRPC::Unavailable, GRPC::DeadlineExceeded => e + # Handle Gitaly connection issues gracefully + Gitlab::ErrorTracking + .track_exception(e, project_id: project.id) end def load_from_project diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb index a737d5543ad..3a05feee156 100644 --- a/lib/gitlab/ci/ansi2html.rb +++ b/lib/gitlab/ci/ansi2html.rb @@ -15,7 +15,7 @@ module Gitlab 4 => 'blue', 5 => 'magenta', 6 => 'cyan', - 7 => 'white', # not that this is gray in the dark (aka default) color table + 7 => 'white' # not that this is gray in the dark (aka default) color table }.freeze STYLE_SWITCHES = { diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb index c705b6f86c7..a500a0cc35d 100644 --- a/lib/gitlab/ci/build/rules.rb +++ b/lib/gitlab/ci/build/rules.rb @@ -6,11 +6,12 @@ module Gitlab class Rules include ::Gitlab::Utils::StrongMemoize - Result = Struct.new(:when, :start_in) do + Result = Struct.new(:when, :start_in, :allow_failure) do def build_attributes { when: self.when, - options: { start_in: start_in }.compact + options: { start_in: start_in }.compact, + allow_failure: allow_failure }.compact end @@ -30,7 +31,8 @@ module Gitlab elsif matched_rule = match_rule(pipeline, context) Result.new( matched_rule.attributes[:when] || @default_when, - matched_rule.attributes[:start_in] + matched_rule.attributes[:start_in], + matched_rule.attributes[:allow_failure] ) else Result.new('never') diff --git a/lib/gitlab/ci/config/entry/bridge.rb b/lib/gitlab/ci/config/entry/bridge.rb new file mode 100644 index 00000000000..c0247dca73d --- /dev/null +++ b/lib/gitlab/ci/config/entry/bridge.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a CI/CD Bridge job that is responsible for + # defining a downstream project trigger. + # + class Bridge < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Configurable + include ::Gitlab::Config::Entry::Attributable + include ::Gitlab::Config::Entry::Inheritable + + ALLOWED_KEYS = %i[trigger stage allow_failure only except + when extends variables needs rules].freeze + + validations do + validates :config, allowed_keys: ALLOWED_KEYS + validates :config, presence: true + validates :name, presence: true + validates :name, type: Symbol + validates :config, disallowed_keys: { + in: %i[only except when start_in], + message: 'key may not be used with `rules`' + }, + if: :has_rules? + + with_options allow_nil: true do + validates :when, + inclusion: { in: %w[on_success on_failure always], + message: 'should be on_success, on_failure or always' } + validates :extends, type: String + validates :rules, array_of_hashes: true + end + + validate on: :composed do + unless trigger.present? || bridge_needs.present? + errors.add(:config, 'should contain either a trigger or a needs:pipeline') + end + end + + validate on: :composed do + next unless bridge_needs.present? + next if bridge_needs.one? + + errors.add(:config, 'should contain at most one bridge need') + end + end + + entry :trigger, ::Gitlab::Ci::Config::Entry::Trigger, + description: 'CI/CD Bridge downstream trigger definition.', + inherit: false + + entry :needs, ::Gitlab::Ci::Config::Entry::Needs, + description: 'CI/CD Bridge needs dependency definition.', + inherit: false, + metadata: { allowed_needs: %i[job bridge] } + + entry :stage, ::Gitlab::Ci::Config::Entry::Stage, + description: 'Pipeline stage this job will be executed into.', + inherit: false + + entry :only, ::Gitlab::Ci::Config::Entry::Policy, + description: 'Refs policy this job will be executed for.', + default: ::Gitlab::Ci::Config::Entry::Policy::DEFAULT_ONLY, + inherit: false + + entry :except, ::Gitlab::Ci::Config::Entry::Policy, + description: 'Refs policy this job will be executed for.', + inherit: false + + entry :rules, ::Gitlab::Ci::Config::Entry::Rules, + description: 'List of evaluable Rules to determine job inclusion.', + inherit: false, + metadata: { + allowed_when: %w[on_success on_failure always never manual delayed].freeze + } + + entry :variables, ::Gitlab::Ci::Config::Entry::Variables, + description: 'Environment variables available for this job.', + inherit: false + + helpers(*ALLOWED_KEYS) + attributes(*ALLOWED_KEYS) + + def self.matching?(name, config) + !name.to_s.start_with?('.') && + config.is_a?(Hash) && + (config.key?(:trigger) || config.key?(:needs)) + end + + def self.visible? + true + end + + def compose!(deps = nil) + super do + has_workflow_rules = deps&.workflow&.has_rules? + + # If workflow:rules: or rules: are used + # they are considered not compatible + # with `only/except` defaults + # + # Context: https://gitlab.com/gitlab-org/gitlab/merge_requests/21742 + if has_rules? || has_workflow_rules + # Remove only/except defaults + # defaults are not considered as defined + @entries.delete(:only) unless only_defined? + @entries.delete(:except) unless except_defined? + end + end + end + + def has_rules? + @config&.key?(:rules) + end + + def name + @metadata[:name] + end + + def value + { name: name, + trigger: (trigger_value if trigger_defined?), + needs: (needs_value if needs_defined?), + ignore: !!allow_failure, + stage: stage_value, + when: when_value, + extends: extends_value, + variables: (variables_value if variables_defined?), + rules: (rules_value if has_rules?), + only: only_value, + except: except_value, + scheduling_type: needs_defined? && !bridge_needs ? :dag : :stage }.compact + end + + def bridge_needs + needs_value[:bridge] if needs_value + end + + private + + def overwrite_entry(deps, key, current_entry) + deps.default[key] unless current_entry.specified? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 124581c961f..ffc8cb887e8 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -258,7 +258,8 @@ module Gitlab after_script: after_script_value, ignore: ignored?, needs: needs_defined? ? needs_value : nil, - resource_group: resource_group } + resource_group: resource_group, + scheduling_type: needs_defined? ? :dag : :stage } end end end diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb index b517dae4d2e..1d3036189b0 100644 --- a/lib/gitlab/ci/config/entry/jobs.rb +++ b/lib/gitlab/ci/config/entry/jobs.rb @@ -36,7 +36,7 @@ module Gitlab end end - TYPES = [Entry::Hidden, Entry::Job].freeze + TYPES = [Entry::Hidden, Entry::Job, Entry::Bridge].freeze private_constant :TYPES @@ -77,5 +77,3 @@ module Gitlab end end end - -::Gitlab::Ci::Config::Entry::Jobs.prepend_if_ee('::EE::Gitlab::Ci::Config::Entry::Jobs') diff --git a/lib/gitlab/ci/config/entry/needs.rb b/lib/gitlab/ci/config/entry/needs.rb index 5301c453ed4..d7ba8624882 100644 --- a/lib/gitlab/ci/config/entry/needs.rb +++ b/lib/gitlab/ci/config/entry/needs.rb @@ -11,12 +11,14 @@ module Gitlab include ::Gitlab::Config::Entry::Validatable validations do - validates :config, presence: true - validate do unless config.is_a?(Hash) || config.is_a?(Array) errors.add(:config, 'can only be a Hash or an Array') end + + if config.is_a?(Hash) && config.empty? + errors.add(:config, 'can not be an empty Hash') + end end validate on: :composed do diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb index f984d7d397a..571e056e096 100644 --- a/lib/gitlab/ci/config/entry/reports.rb +++ b/lib/gitlab/ci/config/entry/reports.rb @@ -11,7 +11,7 @@ module Gitlab include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Attributable - ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management license_scanning metrics].freeze + ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management license_scanning metrics lsif].freeze attributes ALLOWED_KEYS @@ -30,6 +30,7 @@ module Gitlab validates :license_management, array_of_strings_or_string: true validates :license_scanning, array_of_strings_or_string: true validates :metrics, array_of_strings_or_string: true + validates :lsif, array_of_strings_or_string: true end end diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb index 59e0ef583ae..8ffd49b8a93 100644 --- a/lib/gitlab/ci/config/entry/rules/rule.rb +++ b/lib/gitlab/ci/config/entry/rules/rule.rb @@ -9,10 +9,10 @@ module Gitlab include ::Gitlab::Config::Entry::Attributable CLAUSES = %i[if changes exists].freeze - ALLOWED_KEYS = %i[if changes exists when start_in].freeze + ALLOWED_KEYS = %i[if changes exists when start_in allow_failure].freeze ALLOWABLE_WHEN = %w[on_success on_failure always never manual delayed].freeze - attributes :if, :changes, :exists, :when, :start_in + attributes :if, :changes, :exists, :when, :start_in, :allow_failure validations do validates :config, presence: true @@ -26,6 +26,7 @@ module Gitlab validates :if, expression: true validates :changes, :exists, array_of_strings: true, length: { maximum: 50 } validates :when, allowed_values: { in: ALLOWABLE_WHEN } + validates :allow_failure, boolean: true end validate do diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb new file mode 100644 index 00000000000..7202784842a --- /dev/null +++ b/lib/gitlab/ci/config/entry/trigger.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a cross-project downstream trigger. + # + class Trigger < ::Gitlab::Config::Entry::Simplifiable + strategy :SimpleTrigger, if: -> (config) { config.is_a?(String) } + strategy :ComplexTrigger, if: -> (config) { config.is_a?(Hash) } + + class SimpleTrigger < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + validations { validates :config, presence: true } + + def value + { project: @config } + end + end + + class ComplexTrigger < ::Gitlab::Config::Entry::Simplifiable + strategy :CrossProjectTrigger, if: -> (config) { !config.key?(:include) } + + strategy :SameProjectTrigger, if: -> (config) do + ::Feature.enabled?(:ci_parent_child_pipeline, default_enabled: true) && + config.key?(:include) + end + + class CrossProjectTrigger < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + + ALLOWED_KEYS = %i[project branch strategy].freeze + attributes :project, :branch, :strategy + + validations do + validates :config, presence: true + validates :config, allowed_keys: ALLOWED_KEYS + validates :project, presence: true + validates :branch, type: String, allow_nil: true + validates :strategy, type: String, inclusion: { in: %w[depend], message: 'should be depend' }, allow_nil: true + end + end + + class SameProjectTrigger < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + include ::Gitlab::Config::Entry::Configurable + + INCLUDE_MAX_SIZE = 3 + ALLOWED_KEYS = %i[strategy include].freeze + attributes :strategy + + validations do + validates :config, presence: true + validates :config, allowed_keys: ALLOWED_KEYS + validates :strategy, type: String, inclusion: { in: %w[depend], message: 'should be depend' }, allow_nil: true + end + + entry :include, ::Gitlab::Ci::Config::Entry::Includes, + description: 'List of external YAML files to include.', + reserved: true, + metadata: { max_size: INCLUDE_MAX_SIZE } + + def value + @config + end + end + + class UnknownStrategy < ::Gitlab::Config::Entry::Node + def errors + if ::Feature.enabled?(:ci_parent_child_pipeline, default_enabled: true) + ['config must specify either project or include'] + else + ['config must specify project'] + end + end + end + end + + class UnknownStrategy < ::Gitlab::Config::Entry::Node + def errors + ["#{location} has to be either a string or a hash"] + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/external/file/template.rb b/lib/gitlab/ci/config/external/file/template.rb index db56f6a9b00..c4b4a7a0a73 100644 --- a/lib/gitlab/ci/config/external/file/template.rb +++ b/lib/gitlab/ci/config/external/file/template.rb @@ -33,7 +33,7 @@ module Gitlab def template_name return unless template_name_valid? - location.first(-SUFFIX.length) + location.delete_suffix(SUFFIX) end def template_name_valid? diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb index 8f8cae0b5f2..133eb16a83e 100644 --- a/lib/gitlab/ci/parsers/test/junit.rb +++ b/lib/gitlab/ci/parsers/test/junit.rb @@ -50,10 +50,7 @@ module Gitlab status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED system_output = data['failure'] elsif data['error'] - # For now, as an MVC, we are grouping error test cases together - # with failed ones. But we will improve this further on - # https://gitlab.com/gitlab-org/gitlab/issues/32046. - status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED + status = ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR system_output = data['error'] else status = ::Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS diff --git a/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb index 54be789988c..8d19a73dfc3 100644 --- a/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb +++ b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb @@ -31,9 +31,7 @@ module Gitlab end def beta_enabled? - Feature.enabled?(:auto_devops_beta, project, default_enabled: true) && - # workflow:rules are required by `Beta/Auto-DevOps.gitlab-ci.yml` - Feature.enabled?(:workflow_rules, project, default_enabled: true) + Feature.enabled?(:auto_devops_beta, project, default_enabled: true) end end end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb index b282886a56f..c72b5f18424 100644 --- a/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb +++ b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb @@ -31,9 +31,7 @@ module Gitlab end def beta_enabled? - Feature.enabled?(:auto_devops_beta, project, default_enabled: true) && - # workflow:rules are required by `Beta/Auto-DevOps.gitlab-ci.yml` - Feature.enabled?(:workflow_rules, project, default_enabled: true) + Feature.enabled?(:auto_devops_beta, project, default_enabled: true) end end end diff --git a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb index 81f5733b279..a793ae9cc24 100644 --- a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb +++ b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb @@ -9,17 +9,7 @@ module Gitlab include Chain::Helpers def perform! - unless feature_enabled? - if has_workflow_rules? - error("Workflow rules are disabled", config_error: true) - end - - return - end - - unless workflow_passed? - error('Pipeline filtered out by workflow rules.') - end + error('Pipeline filtered out by workflow rules.') unless workflow_passed? end def break? @@ -28,10 +18,6 @@ module Gitlab private - def feature_enabled? - Feature.enabled?(:workflow_rules, @pipeline.project, default_enabled: true) - end - def workflow_passed? strong_memoize(:workflow_passed) do workflow_rules.evaluate(@pipeline, global_context).pass? diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb index 3a40c7b167c..f9ae37aa273 100644 --- a/lib/gitlab/ci/pipeline/chain/populate.rb +++ b/lib/gitlab/ci/pipeline/chain/populate.rb @@ -17,7 +17,7 @@ module Gitlab # pipeline.stages = @command.stage_seeds.map(&:to_resource) - if pipeline.stages.none? + if stage_names.empty? return error('No stages / jobs for this pipeline.') end @@ -31,6 +31,15 @@ module Gitlab def break? pipeline.errors.any? end + + private + + def stage_names + # We filter out `.pre/.post` stages, as they alone are not considered + # a complete pipeline: + # https://gitlab.com/gitlab-org/gitlab/issues/198518 + pipeline.stages.map(&:name) - ::Gitlab::Ci::Config::EdgeStagesInjector::EDGES + end end end end diff --git a/lib/gitlab/ci/pipeline/seed/deployment.rb b/lib/gitlab/ci/pipeline/seed/deployment.rb index 8c90f03cb1d..cc63fb4c609 100644 --- a/lib/gitlab/ci/pipeline/seed/deployment.rb +++ b/lib/gitlab/ci/pipeline/seed/deployment.rb @@ -24,8 +24,14 @@ module Gitlab # non-environment job. return unless deployment.valid? && deployment.environment.persisted? - deployment.cluster_id = - deployment.environment.deployment_platform&.cluster_id + if cluster_id = deployment.environment.deployment_platform&.cluster_id + # double write cluster_id until 12.9: https://gitlab.com/gitlab-org/gitlab/issues/202628 + deployment.cluster_id = cluster_id + deployment.deployment_cluster = ::DeploymentCluster.new( + cluster_id: cluster_id, + kubernetes_namespace: deployment.environment.deployment_namespace + ) + end # Allocate IID for deployments. # This operation must be outside of transactions of pipeline creations. diff --git a/lib/gitlab/ci/reports/test_reports_comparer.rb b/lib/gitlab/ci/reports/test_reports_comparer.rb index 11810bdc0a8..c6f17f0764f 100644 --- a/lib/gitlab/ci/reports/test_reports_comparer.rb +++ b/lib/gitlab/ci/reports/test_reports_comparer.rb @@ -29,7 +29,7 @@ module Gitlab end end - %w(total_count resolved_count failed_count).each do |method| + %w(total_count resolved_count failed_count error_count).each do |method| define_method(method) do # rubocop: disable CodeReuse/ActiveRecord suite_comparers.sum { |suite| suite.public_send(method) } # rubocop:disable GitlabSecurity/PublicSend diff --git a/lib/gitlab/ci/reports/test_suite_comparer.rb b/lib/gitlab/ci/reports/test_suite_comparer.rb index 9cb7db5934c..a58de43e55e 100644 --- a/lib/gitlab/ci/reports/test_suite_comparer.rb +++ b/lib/gitlab/ci/reports/test_suite_comparer.rb @@ -38,6 +38,30 @@ module Gitlab end end + def new_errors + strong_memoize(:new_errors) do + head_suite.error.reject do |key, _| + base_suite.error.include?(key) + end.values + end + end + + def existing_errors + strong_memoize(:existing_errors) do + head_suite.error.select do |key, _| + base_suite.error.include?(key) + end.values + end + end + + def resolved_errors + strong_memoize(:resolved_errors) do + head_suite.success.select do |key, _| + base_suite.error.include?(key) + end.values + end + end + def total_count head_suite.total_count end @@ -47,12 +71,16 @@ module Gitlab end def resolved_count - resolved_failures.count + resolved_failures.count + resolved_errors.count end def failed_count new_failures.count + existing_failures.count end + + def error_count + new_errors.count + existing_errors.count + end end end end diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb index 910d93f54ce..b0b01538a30 100644 --- a/lib/gitlab/ci/status/build/failed.rb +++ b/lib/gitlab/ci/status/build/failed.rb @@ -18,7 +18,13 @@ module Gitlab archived_failure: 'archived failure', unmet_prerequisites: 'unmet prerequisites', scheduler_failure: 'scheduler failure', - data_integrity_failure: 'data integrity failure' + data_integrity_failure: 'data integrity failure', + forward_deployment_failure: 'forward deployment failure', + invalid_bridge_trigger: 'downstream pipeline trigger definition is invalid', + downstream_bridge_project_not_found: 'downstream project could not be found', + 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' }.freeze private_constant :REASONS 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 426f0238f9d..c3ca44eea9e 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 @@ -1,11 +1,13 @@ performance: stage: performance - image: docker:stable + # pin to a version matching the dind service, just to be safe + image: docker:19.03.5 allow_failure: true variables: DOCKER_TLS_CERTDIR: "" services: - - docker:stable-dind + # pin to a known working version until https://gitlab.com/gitlab-org/gitlab-runner/issues/6697 is fixed + - docker:19.03.5-dind script: - | if ! docker info &>/dev/null; then diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml index 8061da968ed..488945ffa3e 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml @@ -4,7 +4,8 @@ build: variables: DOCKER_TLS_CERTDIR: "" services: - - docker:stable-dind + # pin to a known working version until https://gitlab.com/gitlab-org/gitlab-runner/issues/6697 is fixed + - docker:19.03.5-dind script: - | if [[ -z "$CI_COMMIT_TAG" ]]; then 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 8bc60a36ebd..dd5144e28a7 100644 --- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml @@ -1,9 +1,11 @@ code_quality: stage: test - image: docker:stable + # pin to a version matching the dind service, just to be safe + image: docker:19.03.5 allow_failure: true services: - - docker:stable-dind + # pin to a known working version until https://gitlab.com/gitlab-org/gitlab-runner/issues/6697 is fixed + - docker:19.03.5-dind variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml index feedb0994c2..78ee9b28605 100644 --- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ .dast-auto-deploy: - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.8.3" + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.9.1" dast_environment_deploy: extends: .dast-auto-deploy diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml index d20d04425f6..47cc6caa192 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ .auto-deploy: - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.8.3" + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.9.3" review: extends: .auto-deploy diff --git a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml index b9fe838d1da..a0ddd273552 100644 --- a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml @@ -1,6 +1,6 @@ test: services: - - postgres:latest + - "postgres:${POSTGRES_VERSION}" variables: POSTGRES_DB: test stage: test diff --git a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml index 93c69772b01..73ae63c3092 100644 --- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml @@ -1,6 +1,6 @@ apply: stage: deploy - image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.5.0" + image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.8.0" environment: name: production variables: @@ -10,6 +10,10 @@ apply: 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 + 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 script: - gitlab-managed-apps /usr/local/share/gitlab-managed-apps/helmfile.yaml only: 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 225fb7e5606..5ff6413898f 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -57,6 +57,8 @@ dependency_scanning: PIP_REQUIREMENTS_FILE \ MAVEN_CLI_OPTS \ BUNDLER_AUDIT_UPDATE_DISABLED \ + BUNDLER_AUDIT_ADVISORY_DB_URL \ + BUNDLER_AUDIT_ADVISORY_DB_REF_NAME \ ) \ --volume "$PWD:/code" \ --volume /var/run/docker.sock:/var/run/docker.sock \ diff --git a/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml index f10a445f7c9..58fd018a82d 100644 --- a/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml @@ -1,8 +1,5 @@ -# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/license_management/ -# -# Configure the scanning tool through the environment variables. -# List of the variables: https://gitlab.com/gitlab-org/security-products/license-management#settings -# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables +# Deprecated: https://gitlab.com/gitlab-org/gitlab/issues/14624 +# Please, use License-Scanning.gitlab-ci.yml template instead variables: LICENSE_MANAGEMENT_SETUP_CMD: '' # If needed, specify a command to setup your environment with a custom package manager. @@ -16,6 +13,7 @@ license_management: SETUP_CMD: $LICENSE_MANAGEMENT_SETUP_CMD allow_failure: true script: + - echo "This template is deprecated, please use License-Scanning.gitlab-ci.yml template instead." - /run.sh analyze . artifacts: reports: diff --git a/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml new file mode 100644 index 00000000000..2333fb4e947 --- /dev/null +++ b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml @@ -0,0 +1,33 @@ +# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/license_compliance/ +# +# Configure the scanning tool through the environment variables. +# List of the variables: https://gitlab.com/gitlab-org/security-products/license-management#settings +# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables + +variables: + LICENSE_MANAGEMENT_SETUP_CMD: '' # If needed, specify a command to setup your environment with a custom package manager. + +license_scanning: + stage: test + image: + name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable" + entrypoint: [""] + variables: + SETUP_CMD: $LICENSE_MANAGEMENT_SETUP_CMD + allow_failure: true + script: + - /run.sh analyze . + after_script: + - mv gl-license-management-report.json gl-license-scanning-report.json + artifacts: + reports: + license_scanning: gl-license-scanning-report.json + dependencies: [] + only: + refs: + - branches + variables: + - $GITLAB_FEATURES =~ /\blicense_scanning\b/ + except: + variables: + - $LICENSE_MANAGEMENT_DISABLED diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml index 864e3eb569d..51a1f4e549b 100644 --- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -37,11 +37,8 @@ sast: fi fi - | - printenv | grep -E '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | cut -d'=' -f1 | \ - (while IFS='\\n' read -r VAR; do unset -v "$VAR"; done; /bin/printenv > .env) - - | - docker run \ - --env-file .env \ + ENVS=`printenv | grep -vE '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | sed -n '/^[^\t]/s/=.*//p' | sed '/^$/d' | sed 's/^/-e /g' | tr '\n' ' '` + docker run "$ENVS" \ --volume "$PWD:/code" \ --volume /var/run/docker.sock:/var/run/docker.sock \ "registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index 080a8ac107d..ae3ff4a51e2 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -65,6 +65,7 @@ module Gitlab rules: job[:rules], cache: job[:cache], resource_group_key: job[:resource_group], + scheduling_type: job[:scheduling_type], options: { image: job[:image], services: job[:services], diff --git a/lib/gitlab/color_schemes.rb b/lib/gitlab/color_schemes.rb index 881e5dbc923..620b4a8aee6 100644 --- a/lib/gitlab/color_schemes.rb +++ b/lib/gitlab/color_schemes.rb @@ -66,5 +66,9 @@ module Gitlab default end end + + def self.valid_ids + SCHEMES.map(&:id) + end end end diff --git a/lib/gitlab/content_disposition.rb b/lib/gitlab/content_disposition.rb deleted file mode 100644 index ff6154a5b26..00000000000 --- a/lib/gitlab/content_disposition.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true -# This ports ActionDispatch::Http::ContentDisposition (https://github.com/rails/rails/pull/33829, -# which will be available in Rails 6. -module Gitlab - class ContentDisposition # :nodoc: - # Make sure we remove this patch starting with Rails 6.0. - if Rails.version.start_with?('6.0') - raise <<~MSG - Please remove this file and use `ActionDispatch::Http::ContentDisposition` instead. - MSG - end - - def self.format(disposition:, filename:) - new(disposition: disposition, filename: filename).to_s - end - - attr_reader :disposition, :filename - - def initialize(disposition:, filename:) - @disposition = disposition - @filename = filename - end - - # rubocop:disable Style/VariableInterpolation - TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/.freeze - - def ascii_filename - 'filename="' + percent_escape(::I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"' - end - - RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/.freeze - # rubocop:enable Style/VariableInterpolation - - def utf8_filename - "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR) - end - - def to_s - if filename - "#{disposition}; #{ascii_filename}; #{utf8_filename}" - else - "#{disposition}" - end - end - - private - - def percent_escape(string, pattern) - string.gsub(pattern) do |char| - char.bytes.map { |byte| "%%%02X" % byte }.join - end - end - end -end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 6ce47650562..891fd8c1bb5 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -50,7 +50,7 @@ module Gitlab # need to be added to the application settings. To prevent Rake tasks # and other callers from failing, use any loaded settings and return # defaults for missing columns. - if ActiveRecord::Base.connection.migration_context.needs_migration? + if Gitlab::Runtime.rake? && ActiveRecord::Base.connection.migration_context.needs_migration? db_attributes = current_settings&.attributes || {} fake_application_settings(db_attributes) elsif current_settings.present? diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index 1cd54238bb4..06f0cbed147 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -50,7 +50,7 @@ module Gitlab # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). # We compute the (end_time - start_time) interval, and give it an alias based on the current - # cycle analytics stage. + # value stream analytics stage. median_datetime(cte_table, interval_query(project_ids), name) end diff --git a/lib/gitlab/cycle_analytics/usage_data.rb b/lib/gitlab/cycle_analytics/usage_data.rb index 644300caead..acfb641aeec 100644 --- a/lib/gitlab/cycle_analytics/usage_data.rb +++ b/lib/gitlab/cycle_analytics/usage_data.rb @@ -12,7 +12,7 @@ module Gitlab @options = { from: 7.days.ago } end - def to_json + def to_json(*) total = 0 values = diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb index f7b7db50b2f..e6702c5a38b 100644 --- a/lib/gitlab/data_builder/build.rb +++ b/lib/gitlab/data_builder/build.rb @@ -38,11 +38,7 @@ module Gitlab project_id: project.id, project_name: project.full_name, - user: { - id: user.try(:id), - name: user.try(:name), - email: user.try(:email) - }, + user: user.try(:hook_attrs), commit: { # note: commit.id is actually the pipeline id diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index 65cfd47e1e8..41ceeb329b3 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -37,7 +37,7 @@ module Gitlab id: "c5feabde2d8cd023215af4d2ceeb7a64839fc428", message: "Add simple search to projects in public area", timestamp: "2013-05-13T18:18:08+00:00", - url: "https://test.example.com/gitlab/gitlab/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428", + url: "https://test.example.com/gitlab/gitlab/-/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428", author: { name: "Test User", email: "test@example.com" diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 82ec740ade1..02005be1f6a 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -204,15 +204,16 @@ module Gitlab # pool_size - The size of the DB pool. # host - An optional host name to use instead of the default one. def self.create_connection_pool(pool_size, host = nil, port = nil) - # See activerecord-4.2.7.1/lib/active_record/connection_adapters/connection_specification.rb env = Rails.env - original_config = ActiveRecord::Base.configurations + original_config = ActiveRecord::Base.configurations.to_h env_config = original_config[env].merge('pool' => pool_size) env_config['host'] = host if host env_config['port'] = port if port - config = original_config.merge(env => env_config) + config = ActiveRecord::DatabaseConfigurations.new( + original_config.merge(env => env_config) + ) spec = ActiveRecord:: @@ -232,7 +233,7 @@ module Gitlab end def self.cached_table_exists?(table_name) - connection.schema_cache.data_source_exists?(table_name) + exists? && connection.schema_cache.data_source_exists?(table_name) end def self.database_version diff --git a/lib/gitlab/database/batch_count.rb b/lib/gitlab/database/batch_count.rb new file mode 100644 index 00000000000..a9d4665bc5f --- /dev/null +++ b/lib/gitlab/database/batch_count.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +# For large tables, PostgreSQL can take a long time to count rows due to MVCC. +# Implements a distinct and ordinary batch counter +# Needs indexes on the column below to calculate max, min and range queries +# For larger tables just set use higher batch_size with index optimization +# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705 +# Examples: +# extend ::Gitlab::Database::BatchCount +# batch_count(User.active) +# batch_count(::Clusters::Cluster.aws_installed.enabled, :cluster_id) +# batch_distinct_count(::Project, :creator_id) +module Gitlab + module Database + module BatchCount + def batch_count(relation, column = nil, batch_size: nil) + BatchCounter.new(relation, column: column).count(batch_size: batch_size) + end + + def batch_distinct_count(relation, column = nil, batch_size: nil) + BatchCounter.new(relation, column: column).count(mode: :distinct, batch_size: batch_size) + end + + class << self + include BatchCount + end + end + + class BatchCounter + FALLBACK = -1 + MIN_REQUIRED_BATCH_SIZE = 2_000 + MAX_ALLOWED_LOOPS = 10_000 + SLEEP_TIME_IN_SECONDS = 0.01 # 10 msec sleep + # Each query should take <<500ms https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705 + DEFAULT_DISTINCT_BATCH_SIZE = 100_000 + DEFAULT_BATCH_SIZE = 10_000 + + def initialize(relation, column: nil) + @relation = relation + @column = column || relation.primary_key + end + + def unwanted_configuration?(finish, batch_size, start) + batch_size <= MIN_REQUIRED_BATCH_SIZE || + (finish - start) / batch_size >= MAX_ALLOWED_LOOPS || + start > finish + end + + def count(batch_size: nil, mode: :itself) + raise 'BatchCount can not be run inside a transaction' if ActiveRecord::Base.connection.transaction_open? + raise "The mode #{mode.inspect} is not supported" unless [:itself, :distinct].include?(mode) + + # non-distinct have better performance + batch_size ||= mode == :distinct ? DEFAULT_BATCH_SIZE : DEFAULT_DISTINCT_BATCH_SIZE + + start = @relation.minimum(@column) || 0 + finish = @relation.maximum(@column) || 0 + + raise "Batch counting expects positive values only for #{@column}" if start < 0 || finish < 0 + return FALLBACK if unwanted_configuration?(finish, batch_size, start) + + counter = 0 + batch_start = start + + while batch_start <= finish + begin + counter += batch_fetch(batch_start, batch_start + batch_size, mode) + batch_start += batch_size + rescue ActiveRecord::QueryCanceled + # retry with a safe batch size & warmer cache + if batch_size >= 2 * MIN_REQUIRED_BATCH_SIZE + batch_size /= 2 + else + return FALLBACK + end + end + sleep(SLEEP_TIME_IN_SECONDS) + end + + counter + end + + def batch_fetch(start, finish, mode) + # rubocop:disable GitlabSecurity/PublicSend + @relation.select(@column).public_send(mode).where(@column => start..(finish - 1)).count + end + end + end +end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index b7d510c19f9..3b6684b861c 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -280,6 +280,46 @@ module Gitlab end end + # Executes the block with a retry mechanism that alters the +lock_timeout+ and +sleep_time+ between attempts. + # The timings can be controlled via the +timing_configuration+ parameter. + # If the lock was not acquired within the retry period, a last attempt is made without using +lock_timeout+. + # + # ==== Examples + # # Invoking without parameters + # with_lock_retries do + # drop_table :my_table + # end + # + # # Invoking with custom +timing_configuration+ + # t = [ + # [1.second, 1.second], + # [2.seconds, 2.seconds] + # ] + # + # with_lock_retries(timing_configuration: t) do + # drop_table :my_table # this will be retried twice + # end + # + # # Disabling the retries using an environment variable + # > export DISABLE_LOCK_RETRIES=true + # + # with_lock_retries do + # drop_table :my_table # one invocation, it will not retry at all + # end + # + # ==== Parameters + # * +timing_configuration+ - [[ActiveSupport::Duration, ActiveSupport::Duration], ...] lock timeout for the block, sleep time before the next iteration, defaults to `Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION` + # * +logger+ - [Gitlab::JsonLogger] + # * +env+ - [Hash] custom environment hash, see the example with `DISABLE_LOCK_RETRIES` + def with_lock_retries(**args, &block) + merged_args = { + klass: self.class, + logger: Gitlab::BackgroundMigration::Logger + }.merge(args) + + Gitlab::Database::WithLockRetries.new(merged_args).run(&block) + end + def true_value Database.true_value end @@ -342,7 +382,7 @@ module Gitlab count_arel = table.project(Arel.star.count.as('count')) count_arel = yield table, count_arel if block_given? - total = exec_query(count_arel.to_sql).to_hash.first['count'].to_i + total = exec_query(count_arel.to_sql).to_a.first['count'].to_i return if total == 0 @@ -359,7 +399,7 @@ module Gitlab start_arel = table.project(table[:id]).order(table[:id].asc).take(1) start_arel = yield table, start_arel if block_given? - start_id = exec_query(start_arel.to_sql).to_hash.first['id'].to_i + start_id = exec_query(start_arel.to_sql).to_a.first['id'].to_i loop do stop_arel = table.project(table[:id]) @@ -369,7 +409,7 @@ module Gitlab .skip(batch_size) stop_arel = yield table, stop_arel if block_given? - stop_row = exec_query(stop_arel.to_sql).to_hash.first + stop_row = exec_query(stop_arel.to_sql).to_a.first update_arel = Arel::UpdateManager.new .table(table) @@ -1079,6 +1119,20 @@ into similar problems in the future (e.g. when new tables are created). SQL end + # Note this should only be used with very small tables + def backfill_iids(table) + sql = <<-END + UPDATE #{table} + SET iid = #{table}_with_calculated_iid.iid_num + FROM ( + SELECT id, ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY id ASC) AS iid_num FROM #{table} + ) AS #{table}_with_calculated_iid + WHERE #{table}.id = #{table}_with_calculated_iid.id + END + + execute(sql) + end + private def tables_match?(target_table, foreign_key_table) diff --git a/lib/gitlab/database/sha_attribute.rb b/lib/gitlab/database/sha_attribute.rb index 776e80701f1..bad6d3e2a9b 100644 --- a/lib/gitlab/database/sha_attribute.rb +++ b/lib/gitlab/database/sha_attribute.rb @@ -24,7 +24,14 @@ module Gitlab def serialize(value) arg = value ? [value].pack(PACK_FORMAT) : nil - super(arg) + BINARY_TYPE.new.serialize(arg) + end + + # Casts a SHA1 in hexadecimal to the proper binary format. + def self.serialize(value) + arg = value ? [value].pack(PACK_FORMAT) : nil + + BINARY_TYPE.new.serialize(arg) end end end diff --git a/lib/gitlab/database/subquery.rb b/lib/gitlab/database/subquery.rb deleted file mode 100644 index 2a6f39c6a27..00000000000 --- a/lib/gitlab/database/subquery.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Database - module Subquery - class << self - def self_join(relation) - t = relation.arel_table - t2 = relation.arel.as('t2') - - relation.unscoped.joins(t.join(t2).on(t[:id].eq(t2[:id])).join_sources.first) - end - end - end - end -end diff --git a/lib/gitlab/database/with_lock_retries.rb b/lib/gitlab/database/with_lock_retries.rb new file mode 100644 index 00000000000..2f36bfa1480 --- /dev/null +++ b/lib/gitlab/database/with_lock_retries.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +module Gitlab + module Database + class WithLockRetries + NULL_LOGGER = Gitlab::JsonLogger.new('/dev/null') + + # Each element of the array represents a retry iteration. + # - DEFAULT_TIMING_CONFIGURATION.size provides the iteration count. + # - First element: DB lock_timeout + # - Second element: Sleep time after unsuccessful lock attempt (LockWaitTimeout error raised) + # - Worst case, this configuration would retry for about 40 minutes. + DEFAULT_TIMING_CONFIGURATION = [ + [0.1.seconds, 0.05.seconds], # short timings, lock_timeout: 100ms, sleep after LockWaitTimeout: 50ms + [0.1.seconds, 0.05.seconds], + [0.2.seconds, 0.05.seconds], + [0.3.seconds, 0.10.seconds], + [0.4.seconds, 0.15.seconds], + [0.5.seconds, 2.seconds], + [0.5.seconds, 2.seconds], + [0.5.seconds, 2.seconds], + [0.5.seconds, 2.seconds], + [1.second, 5.seconds], # probably high traffic, increase timings + [1.second, 1.minute], + [0.1.seconds, 0.05.seconds], + [0.1.seconds, 0.05.seconds], + [0.2.seconds, 0.05.seconds], + [0.3.seconds, 0.10.seconds], + [0.4.seconds, 0.15.seconds], + [0.5.seconds, 2.seconds], + [0.5.seconds, 2.seconds], + [0.5.seconds, 2.seconds], + [3.seconds, 3.minutes], # probably high traffic or long locks, increase timings + [0.1.seconds, 0.05.seconds], + [0.1.seconds, 0.05.seconds], + [0.5.seconds, 2.seconds], + [0.5.seconds, 2.seconds], + [5.seconds, 2.minutes], + [0.5.seconds, 0.5.seconds], + [0.5.seconds, 0.5.seconds], + [7.seconds, 5.minutes], + [0.5.seconds, 0.5.seconds], + [0.5.seconds, 0.5.seconds], + [7.seconds, 5.minutes], + [0.5.seconds, 0.5.seconds], + [0.5.seconds, 0.5.seconds], + [7.seconds, 5.minutes], + [0.1.seconds, 0.05.seconds], + [0.1.seconds, 0.05.seconds], + [0.5.seconds, 2.seconds], + [10.seconds, 10.minutes], + [0.1.seconds, 0.05.seconds], + [0.5.seconds, 2.seconds], + [10.seconds, 10.minutes] + ].freeze + + def initialize(logger: NULL_LOGGER, timing_configuration: DEFAULT_TIMING_CONFIGURATION, klass: nil, env: ENV) + @logger = logger + @klass = klass + @timing_configuration = timing_configuration + @env = env + @current_iteration = 1 + @log_params = { method: 'with_lock_retries', class: klass.to_s } + end + + def run(&block) + raise 'no block given' unless block_given? + + @block = block + + if lock_retries_disabled? + log(message: 'DISABLE_LOCK_RETRIES environment variable is true, executing the block without retry') + + return run_block + end + + begin + run_block_with_transaction + rescue ActiveRecord::LockWaitTimeout + if retry_with_lock_timeout? + wait_until_next_retry + + retry + else + run_block_without_lock_timeout + end + end + end + + private + + attr_reader :logger, :env, :block, :current_iteration, :log_params, :timing_configuration + + def run_block + block.call + end + + def run_block_with_transaction + ActiveRecord::Base.transaction(requires_new: true) do + execute("SET LOCAL lock_timeout TO '#{current_lock_timeout_in_ms}ms'") + + log(message: 'Lock timeout is set', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms) + + run_block + + log(message: 'Migration finished', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms) + end + end + + def retry_with_lock_timeout? + current_iteration != retry_count + end + + def wait_until_next_retry + log(message: 'ActiveRecord::LockWaitTimeout error, retrying after sleep', current_iteration: current_iteration, sleep_time_in_seconds: current_sleep_time_in_seconds) + + sleep(current_sleep_time_in_seconds) + + @current_iteration += 1 + end + + def run_block_without_lock_timeout + log(message: "Couldn't acquire lock to perform the migration", current_iteration: current_iteration) + log(message: "Executing the migration without lock timeout", current_iteration: current_iteration) + + execute("SET LOCAL lock_timeout TO '0'") + + run_block + + log(message: 'Migration finished', current_iteration: current_iteration) + end + + def lock_retries_disabled? + Gitlab::Utils.to_boolean(env['DISABLE_LOCK_RETRIES']) + end + + def log(params) + logger.info(log_params.merge(params)) + end + + def execute(statement) + ActiveRecord::Base.connection.execute(statement) + end + + def retry_count + timing_configuration.size + end + + def current_lock_timeout_in_ms + Integer(timing_configuration[current_iteration - 1][0].in_milliseconds) + end + + def current_sleep_time_in_seconds + timing_configuration[current_iteration - 1][1].to_f + end + end + end +end diff --git a/lib/gitlab/database/x509_serial_number_attribute.rb b/lib/gitlab/database/x509_serial_number_attribute.rb new file mode 100644 index 00000000000..e12f64787e7 --- /dev/null +++ b/lib/gitlab/database/x509_serial_number_attribute.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Gitlab + module Database + # Class for casting binary data to int. + # + # Using X509SerialNumberAttribute allows you to store X509 certificate + # serial number values as binary while still using integer to access them. + # rfc 5280 - 4.1.2.2 Serial number (20 octets is the maximum), could be: + # - 1461501637330902918203684832716283019655932542975 + # - 0xffffffffffffffffffffffffffffffffffffffff + class X509SerialNumberAttribute < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Bytea + PACK_FORMAT = 'H*' + + def deserialize(value) + value = super(value) + value ? value.unpack1(PACK_FORMAT).to_i : nil + end + + def serialize(value) + arg = value ? [value.to_s].pack(PACK_FORMAT) : nil + super(arg) + end + end + end +end diff --git a/lib/gitlab/database_importers/common_metrics.rb b/lib/gitlab/database_importers/common_metrics.rb index b9d320f2fc7..f964ae8a275 100644 --- a/lib/gitlab/database_importers/common_metrics.rb +++ b/lib/gitlab/database_importers/common_metrics.rb @@ -6,5 +6,3 @@ module Gitlab end end end - -Gitlab::DatabaseImporters::CommonMetrics.prepend_if_ee('EE::Gitlab::DatabaseImporters::CommonMetrics') 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 409a1252da1..fb0fcc5a93b 100644 --- a/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb +++ b/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb @@ -17,7 +17,9 @@ module Gitlab # custom groups business: 0, response: 1, - system: 2 + system: 2, + + cluster_health: -100 } end @@ -31,12 +33,11 @@ module Gitlab ha_proxy: _('Response metrics (HA Proxy)'), aws_elb: _('Response metrics (AWS ELB)'), nginx: _('Response metrics (NGINX)'), - kubernetes: _('System metrics (Kubernetes)') + kubernetes: _('System metrics (Kubernetes)'), + cluster_health: _('Cluster Health') } end end end end end - -::Gitlab::DatabaseImporters::CommonMetrics::PrometheusMetricEnums.prepend_if_ee('EE::Gitlab::DatabaseImporters::CommonMetrics::PrometheusMetricEnums') diff --git a/lib/gitlab/database_importers/self_monitoring/helpers.rb b/lib/gitlab/database_importers/self_monitoring/helpers.rb index d7e90967e89..6956401e20d 100644 --- a/lib/gitlab/database_importers/self_monitoring/helpers.rb +++ b/lib/gitlab/database_importers/self_monitoring/helpers.rb @@ -13,11 +13,11 @@ module Gitlab end def self_monitoring_project - application_settings.instance_administration_project + application_settings.self_monitoring_project end def self_monitoring_project_id - application_settings.instance_administration_project_id + application_settings.self_monitoring_project_id end 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 d08afeef3b6..07a4c3bf5e6 100644 --- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb +++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb @@ -9,12 +9,13 @@ module Gitlab include SelfMonitoring::Helpers VISIBILITY_LEVEL = Gitlab::VisibilityLevel::INTERNAL - PROJECT_NAME = 'GitLab Instance Administration' + PROJECT_NAME = 'GitLab self monitoring' steps :validate_application_settings, :create_group, :create_project, :save_project_id, + :create_environment, :add_prometheus_manual_configuration, :track_event @@ -69,10 +70,19 @@ module Gitlab return success(result) if project_created? response = application_settings.update( - instance_administration_project_id: result[:project].id + self_monitoring_project_id: result[:project].id ) if response + # In the add_prometheus_manual_configuration method, the Prometheus + # listen_address config is saved as an api_url in the PrometheusService + # model. There are validates hooks in the PrometheusService model that + # check if the project associated with the PrometheusService is the + # self_monitoring project. It checks + # Gitlab::CurrentSettings.self_monitoring_project_id, which is why the + # Gitlab::CurrentSettings cache needs to be expired here, so that + # PrometheusService sees the latest self_monitoring_project_id. + Gitlab::CurrentSettings.expire_current_application_settings success(result) else log_error("Could not save instance administration project ID, errors: %{errors}" % { errors: application_settings.errors.full_messages }) @@ -80,6 +90,19 @@ module Gitlab end end + def create_environment(result) + return success(result) if result[:project].environments.exists? + + environment = ::Environment.new(project_id: result[:project].id, name: 'production') + + if environment.save + success(result) + else + log_error("Could not create environment for the Self monitoring project. Errors: %{errors}" % { errors: environment.errors.full_messages }) + error(_('Could not create environment')) + end + end + def add_prometheus_manual_configuration(result) return success(result) unless prometheus_enabled? return success(result) unless prometheus_listen_address.present? @@ -115,7 +138,7 @@ module Gitlab def docs_path Rails.application.routes.url_helpers.help_page_path( - 'administration/monitoring/gitlab_instance_administration_project/index' + 'administration/monitoring/gitlab_self_monitoring_project/index' ) end diff --git a/lib/gitlab/dependency_linker/godeps_json_linker.rb b/lib/gitlab/dependency_linker/godeps_json_linker.rb index d24c137793e..9166e9091ac 100644 --- a/lib/gitlab/dependency_linker/godeps_json_linker.rb +++ b/lib/gitlab/dependency_linker/godeps_json_linker.rb @@ -12,10 +12,12 @@ module Gitlab def link_dependencies link_json('ImportPath') do |path| case path + when %r{\A(?<repo>github\.com/#{REPO_REGEX})/(?<path>.+)\z} + "https://#{$~[:repo]}/tree/master/#{$~[:path]}" when %r{\A(?<repo>gitlab\.com/#{NESTED_REPO_REGEX})\.git/(?<path>.+)\z}, - %r{\A(?<repo>git(lab|hub)\.com/#{REPO_REGEX})/(?<path>.+)\z} + %r{\A(?<repo>gitlab\.com/#{REPO_REGEX})/(?<path>.+)\z} - "https://#{$~[:repo]}/tree/master/#{$~[:path]}" + "https://#{$~[:repo]}/-/tree/master/#{$~[:path]}" when /\Agolang\.org/ "https://godoc.org/#{path}" else diff --git a/lib/gitlab/diff/deprecated_highlight_cache.rb b/lib/gitlab/diff/deprecated_highlight_cache.rb deleted file mode 100644 index 47347686973..00000000000 --- a/lib/gitlab/diff/deprecated_highlight_cache.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true -# -module Gitlab - module Diff - class DeprecatedHighlightCache - delegate :diffable, to: :@diff_collection - delegate :diff_options, to: :@diff_collection - - def initialize(diff_collection, backend: Rails.cache) - @backend = backend - @diff_collection = diff_collection - end - - # - Reads from cache - # - Assigns DiffFile#highlighted_diff_lines for cached files - def decorate(diff_file) - if content = read_file(diff_file) - diff_file.highlighted_diff_lines = content.map do |line| - Gitlab::Diff::Line.init_from_hash(line) - end - end - end - - # It populates a Hash in order to submit a single write to the memory - # cache. This avoids excessive IO generated by N+1's (1 writing for - # each highlighted line or file). - def write_if_empty - return if cached_content.present? - - @diff_collection.diff_files.each do |diff_file| - next unless cacheable?(diff_file) - - diff_file_id = diff_file.file_identifier - - cached_content[diff_file_id] = diff_file.highlighted_diff_lines.map(&:to_hash) - end - - cache.write(key, cached_content, expires_in: 1.week) - end - - def clear - cache.delete(key) - end - - def key - [diffable, 'highlighted-diff-files', Gitlab::Diff::Line::SERIALIZE_KEYS, diff_options] - end - - private - - def read_file(diff_file) - cached_content[diff_file.file_identifier] - end - - def cache - @backend - end - - def cached_content - @cached_content ||= cache.read(key) || {} - end - - def cacheable?(diff_file) - diffable.present? && diff_file.text? && diff_file.diffable? - end - end - end -end diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb index dc245377ccc..12b93af3f26 100644 --- a/lib/gitlab/diff/diff_refs.rb +++ b/lib/gitlab/diff/diff_refs.rb @@ -37,7 +37,7 @@ module Gitlab # We have `base_sha` directly available on `DiffRefs` because it's faster# # than having to look it up in the repo every time. def complete? - start_sha && head_sha + start_sha.present? && head_sha.present? end def compare_in(project) diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 2ba38f31720..4fc5bfddf0c 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -350,6 +350,12 @@ module Gitlab private + def fetch_blob(sha, path) + return unless sha + + Blob.lazy(repository.project, sha, path) + end + def total_blob_lines(blob) @total_lines ||= begin line_count = blob.lines.size @@ -385,15 +391,11 @@ module Gitlab end def new_blob_lazy - return unless new_content_sha - - Blob.lazy(repository.project, new_content_sha, file_path) + fetch_blob(new_content_sha, file_path) end def old_blob_lazy - return unless old_content_sha - - Blob.lazy(repository.project, old_content_sha, old_path) + fetch_blob(old_content_sha, old_path) end def simple_viewer_class diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb index d27da186de0..d126fdb2be2 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb @@ -47,11 +47,7 @@ module Gitlab private def cache - @cache ||= if Feature.enabled?(:hset_redis_diff_caching, project, default_enabled: true) - Gitlab::Diff::HighlightCache.new(self) - else - Gitlab::Diff::DeprecatedHighlightCache.new(self) - end + @cache ||= Gitlab::Diff::HighlightCache.new(self) end end end diff --git a/lib/gitlab/diff/formatters/image_formatter.rb b/lib/gitlab/diff/formatters/image_formatter.rb index 5bc9f0c337f..d0c13dee1aa 100644 --- a/lib/gitlab/diff/formatters/image_formatter.rb +++ b/lib/gitlab/diff/formatters/image_formatter.rb @@ -23,7 +23,7 @@ module Gitlab end def complete? - x && y && width && height + [x, y, width, height].all?(&:present?) end def to_h @@ -37,7 +37,9 @@ module Gitlab def ==(other) other.is_a?(self.class) && x == other.x && - y == other.y + y == other.y && + width == other.width && + height == other.height end end end diff --git a/lib/gitlab/diff/formatters/text_formatter.rb b/lib/gitlab/diff/formatters/text_formatter.rb index f6e247ef665..5b670b1f83b 100644 --- a/lib/gitlab/diff/formatters/text_formatter.rb +++ b/lib/gitlab/diff/formatters/text_formatter.rb @@ -19,7 +19,7 @@ module Gitlab end def complete? - old_line || new_line + old_line.present? || new_line.present? end def to_h diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb index 403effbb0c6..0a8fbb9a673 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::Metrics::Methods include Gitlab::Utils::StrongMemoize EXPIRATION = 1.week @@ -11,6 +12,11 @@ module Gitlab delegate :diffable, to: :@diff_collection delegate :diff_options, to: :@diff_collection + define_histogram :gitlab_redis_diff_caching_memory_usage_bytes do + docstring 'Redis diff caching memory usage by key' + buckets [100, 1000, 10000, 100000, 1000000, 10000000] + end + def initialize(diff_collection) @diff_collection = diff_collection end @@ -57,17 +63,6 @@ module Gitlab private - # We create a Gitlab::Diff::DeprecatedHighlightCache here in order to - # expire deprecated cache entries while we make the transition. This can - # be removed when :hset_redis_diff_caching is fully launched. - # See https://gitlab.com/gitlab-org/gitlab/issues/38008 - # - def deprecated_cache - strong_memoize(:deprecated_cache) do - Gitlab::Diff::DeprecatedHighlightCache.new(@diff_collection) - end - end - def cacheable_files strong_memoize(:cacheable_files) do diff_files.select { |file| cacheable?(file) && read_file(file).nil? } @@ -104,10 +99,6 @@ module Gitlab # clear_memoization(:cached_content) clear_memoization(:cacheable_files) - - # Clean up any deprecated hash entries - # - deprecated_cache.clear end def file_paths diff --git a/lib/gitlab/diff/suggestion_diff.rb b/lib/gitlab/diff/suggestion_diff.rb index ee153c226b7..783264fe999 100644 --- a/lib/gitlab/diff/suggestion_diff.rb +++ b/lib/gitlab/diff/suggestion_diff.rb @@ -18,7 +18,7 @@ module Gitlab private def raw_diff - "#{diff_header}\n#{from_content_as_diff}#{to_content_as_diff}" + "#{diff_header}\n#{from_content_as_diff}\n#{to_content_as_diff}" end def diff_header @@ -26,7 +26,7 @@ module Gitlab end def from_content_as_diff - from_content.lines.map { |line| line.prepend('-') }.join + from_content.lines.map { |line| line.prepend('-') }.join.delete_suffix("\n") end def to_content_as_diff diff --git a/lib/gitlab/email/attachment_uploader.rb b/lib/gitlab/email/attachment_uploader.rb index 0a14a909e31..d8962ec0d20 100644 --- a/lib/gitlab/email/attachment_uploader.rb +++ b/lib/gitlab/email/attachment_uploader.rb @@ -12,7 +12,7 @@ module Gitlab def execute(upload_parent:, uploader_class:) attachments = [] - message.attachments.each do |attachment| + filter_signature_attachments(message).each do |attachment| tmp = Tempfile.new("gitlab-email-attachment") begin File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded } @@ -32,6 +32,22 @@ module Gitlab attachments end + + private + + # If this is a signed message (e.g. S/MIME or PGP), remove the signature + # from the uploaded attachments + def filter_signature_attachments(message) + attachments = message.attachments + + if message.content_type&.starts_with?('multipart/signed') + signature_protocol = message.content_type_parameters[:protocol] + + attachments.delete_if { |attachment| attachment.content_type.starts_with?(signature_protocol) } if signature_protocol.present? + end + + attachments + end end end end diff --git a/lib/gitlab/email/hook/smime_signature_interceptor.rb b/lib/gitlab/email/hook/smime_signature_interceptor.rb index e48041d9218..61c9c984f8e 100644 --- a/lib/gitlab/email/hook/smime_signature_interceptor.rb +++ b/lib/gitlab/email/hook/smime_signature_interceptor.rb @@ -11,6 +11,7 @@ module Gitlab cert: certificate.cert, key: certificate.key, data: message.encoded) + signed_email = Mail.new(signed_message) overwrite_body(message, signed_email) diff --git a/lib/gitlab/email/smime/certificate.rb b/lib/gitlab/email/smime/certificate.rb index b331c4ca19c..59d7b0c3c5b 100644 --- a/lib/gitlab/email/smime/certificate.rb +++ b/lib/gitlab/email/smime/certificate.rb @@ -4,8 +4,6 @@ module Gitlab module Email module Smime class Certificate - include OpenSSL - attr_reader :key, :cert def key_string @@ -17,8 +15,8 @@ module Gitlab end def self.from_strings(key_string, cert_string) - key = PKey::RSA.new(key_string) - cert = X509::Certificate.new(cert_string) + key = OpenSSL::PKey::RSA.new(key_string) + cert = OpenSSL::X509::Certificate.new(cert_string) new(key, cert) end diff --git a/lib/gitlab/email/smime/signer.rb b/lib/gitlab/email/smime/signer.rb index 2fa83014003..db03e383ecf 100644 --- a/lib/gitlab/email/smime/signer.rb +++ b/lib/gitlab/email/smime/signer.rb @@ -7,20 +7,18 @@ module Gitlab module Smime # Tooling for signing and verifying data with SMIME class Signer - include OpenSSL - def self.sign(cert:, key:, data:) - signed_data = PKCS7.sign(cert, key, data, nil, PKCS7::DETACHED) - PKCS7.write_smime(signed_data) + signed_data = OpenSSL::PKCS7.sign(cert, key, data, nil, OpenSSL::PKCS7::DETACHED) + OpenSSL::PKCS7.write_smime(signed_data) end # return nil if data cannot be verified, otherwise the signed content data def self.verify_signature(cert:, ca_cert: nil, signed_data:) - store = X509::Store.new + store = OpenSSL::X509::Store.new store.set_default_paths store.add_cert(ca_cert) if ca_cert - signed_smime = PKCS7.read_smime(signed_data) + signed_smime = OpenSSL::PKCS7.read_smime(signed_data) signed_smime if signed_smime.verify([cert], store) end end diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb index 6df9bfad657..d20324a613e 100644 --- a/lib/gitlab/error_tracking.rb +++ b/lib/gitlab/error_tracking.rb @@ -97,6 +97,8 @@ module Gitlab extra = extra.merge(data) if data.is_a?(Hash) end + extra = sanitize_request_parameters(extra) + if sentry && Raven.configuration.server Raven.capture_exception(exception, tags: default_tags, extra: extra) end @@ -117,6 +119,11 @@ module Gitlab end end + def sanitize_request_parameters(parameters) + filter = ActiveSupport::ParameterFilter.new(::Rails.application.config.filter_parameters) + filter.filter(parameters) + end + def sentry_dsn return unless Rails.env.production? || Rails.env.development? return unless Gitlab.config.sentry.enabled diff --git a/lib/gitlab/error_tracking/detailed_error.rb b/lib/gitlab/error_tracking/detailed_error.rb index c240ec1fa4f..b49f2472e01 100644 --- a/lib/gitlab/error_tracking/detailed_error.rb +++ b/lib/gitlab/error_tracking/detailed_error.rb @@ -35,7 +35,7 @@ module Gitlab :user_count def self.declarative_policy_class - 'ErrorTracking::DetailedErrorPolicy' + 'ErrorTracking::BasePolicy' end end end diff --git a/lib/gitlab/error_tracking/error.rb b/lib/gitlab/error_tracking/error.rb index 4af5192aa6a..6bfb9dae610 100644 --- a/lib/gitlab/error_tracking/error.rb +++ b/lib/gitlab/error_tracking/error.rb @@ -4,11 +4,16 @@ module Gitlab module ErrorTracking class Error include ActiveModel::Model + include GlobalID::Identification attr_accessor :id, :title, :type, :user_count, :count, :first_seen, :last_seen, :message, :culprit, :external_url, :project_id, :project_name, :project_slug, :short_id, :status, :frequency + + def self.declarative_policy_class + 'ErrorTracking::BasePolicy' + end end end end diff --git a/lib/gitlab/error_tracking/error_collection.rb b/lib/gitlab/error_tracking/error_collection.rb new file mode 100644 index 00000000000..56bcb671363 --- /dev/null +++ b/lib/gitlab/error_tracking/error_collection.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module ErrorTracking + class ErrorCollection + include GlobalID::Identification + + attr_accessor :issues, :external_url, :project + + alias_attribute :gitlab_project, :project + + def initialize(project:, external_url: nil, issues: []) + @project = project + @external_url = external_url + @issues = issues + end + + def self.declarative_policy_class + 'ErrorTracking::BasePolicy' + end + end + end +end diff --git a/lib/gitlab/error_tracking/error_event.rb b/lib/gitlab/error_tracking/error_event.rb index c6e0d82f868..015d2c0ead0 100644 --- a/lib/gitlab/error_tracking/error_event.rb +++ b/lib/gitlab/error_tracking/error_event.rb @@ -5,7 +5,11 @@ module Gitlab class ErrorEvent include ActiveModel::Model - attr_accessor :issue_id, :date_received, :stack_trace_entries + attr_accessor :issue_id, :date_received, :stack_trace_entries, :gitlab_project + + def self.declarative_policy_class + 'ErrorTracking::BasePolicy' + end end end end diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb index a11d6b66409..303e1a23e6b 100644 --- a/lib/gitlab/etag_caching/middleware.rb +++ b/lib/gitlab/etag_caching/middleware.rb @@ -18,7 +18,7 @@ module Gitlab if_none_match = env['HTTP_IF_NONE_MATCH'] if if_none_match == etag - handle_cache_hit(etag, route) + handle_cache_hit(etag, route, request) else track_cache_miss(if_none_match, cached_value_present, route) @@ -47,11 +47,13 @@ module Gitlab %Q{W/"#{value}"} end - def handle_cache_hit(etag, route) + def handle_cache_hit(etag, route, request) track_event(:etag_caching_cache_hit, route) status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429 + add_instrument_for_cache_hit(status_code, route, request) + [status_code, { 'ETag' => etag, 'X-Gitlab-From-Cache' => 'true' }, []] end @@ -68,6 +70,21 @@ module Gitlab def track_event(name, route) Gitlab::Metrics.add_event(name, endpoint: route.name) end + + def add_instrument_for_cache_hit(status, route, request) + payload = { + etag_route: route.name, + params: request.filtered_parameters, + headers: request.headers, + format: request.format.ref, + method: request.request_method, + path: request.filtered_path, + status: status + } + + ActiveSupport::Notifications.instrument( + "process_action.action_controller", payload) + end end end end diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 9d14695c098..7c59267c0b6 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -20,8 +20,14 @@ module Gitlab paid_signup_flow: { feature_toggle: :paid_signup_flow, environment: ::Gitlab.dev_env_or_com?, - enabled_ratio: 0.1, + enabled_ratio: 0.5, tracking_category: 'Growth::Acquisition::Experiment::PaidSignUpFlow' + }, + suggest_pipeline: { + feature_toggle: :suggest_pipeline, + environment: ::Gitlab.dev_env_or_com?, + enabled_ratio: 0.1, + tracking_category: 'Growth::Expansion::Experiment::SuggestPipeline' } }.freeze @@ -53,14 +59,14 @@ module Gitlab Experimentation.enabled_for_user?(experiment_key, experimentation_subject_index) || forced_enabled?(experiment_key) end - def track_experiment_event(experiment_key, action) - track_experiment_event_for(experiment_key, action) do |tracking_data| + def track_experiment_event(experiment_key, action, value = nil) + track_experiment_event_for(experiment_key, action, value) do |tracking_data| ::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), tracking_data) end end - def frontend_experimentation_tracking_data(experiment_key, action) - track_experiment_event_for(experiment_key, action) do |tracking_data| + def frontend_experimentation_tracking_data(experiment_key, action, value = nil) + track_experiment_event_for(experiment_key, action, value) do |tracking_data| gon.push(tracking_data: tracking_data) end end @@ -77,19 +83,20 @@ module Gitlab experimentation_subject_id.delete('-').hex % 100 end - def track_experiment_event_for(experiment_key, action) + def track_experiment_event_for(experiment_key, action, value) return unless Experimentation.enabled?(experiment_key) - yield experimentation_tracking_data(experiment_key, action) + yield experimentation_tracking_data(experiment_key, action, value) end - def experimentation_tracking_data(experiment_key, action) + def experimentation_tracking_data(experiment_key, action, value) { category: tracking_category(experiment_key), action: action, property: tracking_group(experiment_key), - label: experimentation_subject_id - } + label: experimentation_subject_id, + value: value + }.compact end def tracking_category(experiment_key) diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb index a71baadfdb3..d438b0415fa 100644 --- a/lib/gitlab/file_finder.rb +++ b/lib/gitlab/file_finder.rb @@ -13,14 +13,15 @@ module Gitlab @ref = ref end - def find(query) + def find(query, content_match_cutoff: nil) query = Gitlab::Search::Query.new(query, encode_binary: true) do filter :filename, matcher: ->(filter, blob) { blob.binary_path =~ /#{filter[:regex_value]}$/i } filter :path, matcher: ->(filter, blob) { blob.binary_path =~ /#{filter[:regex_value]}/i } filter :extension, matcher: ->(filter, blob) { blob.binary_path =~ /\.#{filter[:regex_value]}$/i } end - files = find_by_path(query.term) + find_by_content(query.term) + content_match_cutoff = nil if query.filters.any? + files = find_by_path(query.term) + find_by_content(query.term, { limit: content_match_cutoff }) files = query.filter_results(files) if query.filters.any? @@ -29,8 +30,8 @@ module Gitlab private - def find_by_content(query) - repository.search_files_by_content(query, ref).map do |result| + def find_by_content(query, options) + repository.search_files_by_content(query, ref, options).map do |result| Gitlab::Search::FoundBlob.new(content_match: result, project: project, ref: ref, repository: repository) end end diff --git a/lib/gitlab/file_hook.rb b/lib/gitlab/file_hook.rb index f886fd10f53..38c19ff506f 100644 --- a/lib/gitlab/file_hook.rb +++ b/lib/gitlab/file_hook.rb @@ -17,7 +17,7 @@ module Gitlab def self.execute_all_async(data) args = files.map { |file| [file, data] } - FileHookWorker.bulk_perform_async(args) + FileHookWorker.bulk_perform_async(args) # rubocop:disable Scalability/BulkPerformWithContext end def self.execute(file, data) diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 10df4ed72d9..f2a6211f270 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -5,6 +5,7 @@ module Gitlab class Blob include Gitlab::BlobHelper include Gitlab::EncodingHelper + include Gitlab::Metrics::Methods extend Gitlab::Git::WrapsGitalyErrors # This number is the maximum amount of data that we want to display to @@ -13,6 +14,11 @@ module Gitlab # use load_all_data!. MAX_DATA_DISPLAY_SIZE = 10.megabytes + # The number of blobs loaded in a single Gitaly call + # When a large number of blobs requested, we'd want to fetch them in + # multiple Gitaly calls + BATCH_SIZE = 250 + # These limits are used as a heuristic to ignore files which can't be LFS # pointers. The format of these is described in # https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md#the-pointer @@ -21,6 +27,14 @@ module Gitlab attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary + define_counter :gitlab_blob_truncated_true do + docstring 'blob.truncated? == true' + end + + define_counter :gitlab_blob_truncated_false do + docstring 'blob.truncated? == false' + end + class << self def find(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE) tree_entry(repository, sha, path, limit) @@ -67,7 +81,13 @@ module Gitlab # to the caller to limit the number of blobs and blob_size_limit. # def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE) - repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a + if Feature.enabled?(:blobs_fetch_in_batches, default_enabled: true) + blob_references.each_slice(BATCH_SIZE).flat_map do |refs| + repository.gitaly_blob_client.get_blobs(refs, blob_size_limit).to_a + end + else + repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a + end end # Returns an array of Blob instances just with the metadata, that means @@ -117,7 +137,7 @@ module Gitlab def load_all_data!(repository) return if @data == '' # don't mess with submodule blobs - # Even if we return early, recalculate wether this blob is binary in + # Even if we return early, recalculate whether this blob is binary in # case a blob was initialized as text but the full data isn't @binary = nil diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 48da838366f..0b999197cd8 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -130,8 +130,7 @@ module Gitlab # :skip is the number of commits to skip # :order is the commits order and allowed value is :none (default), :date, # :topo, or any combination of them (in an array). Commit ordering types - # are documented here: - # http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant) + # are documented here: https://git-scm.com/docs/git-log#_commit_ordering def find_all(repo, options = {}) wrapped_gitaly_errors do Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options) diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index b79e30bff78..46896961867 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -102,7 +102,7 @@ module Gitlab def populate! return if @populated - each { nil } # force a loop through all diffs + each {} # force a loop through all diffs nil end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 0120e3be14c..6bfe744a5cd 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -636,10 +636,9 @@ module Gitlab end # Delete the specified branch from the repository + # Note: No Git hooks are executed for this action def delete_branch(branch_name) - wrapped_gitaly_errors do - gitaly_ref_client.delete_branch(branch_name) - end + write_ref(branch_name, Gitlab::Git::BLANK_SHA) rescue CommandError => e raise DeleteBranchError, e end @@ -651,14 +650,13 @@ module Gitlab end # Create a new branch named **ref+ based on **stat_point+, HEAD by default + # Note: No Git hooks are executed for this action # # Examples: # create_branch("feature") # create_branch("other-feature", "master") def create_branch(ref, start_point = "HEAD") - wrapped_gitaly_errors do - gitaly_ref_client.create_branch(ref, start_point) - end + write_ref(ref, start_point) end # If `mirror_refmap` is present the remote is set as mirror with that mapping @@ -822,17 +820,6 @@ module Gitlab gitaly_repository_client.create_from_snapshot(url, auth) end - # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628 - def rebase_deprecated(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) - wrapped_gitaly_errors do - gitaly_operation_client.user_rebase(user, rebase_id, - branch: branch, - branch_sha: branch_sha, - remote_repository: remote_repository, - remote_branch: remote_branch) - end - end - def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:, push_options: [], &block) wrapped_gitaly_errors do gitaly_operation_client.rebase( @@ -969,13 +956,13 @@ module Gitlab gitaly_ref_client.tag_names_contains_sha(sha) end - def search_files_by_content(query, ref) + def search_files_by_content(query, ref, options = {}) return [] if empty? || query.blank? safe_query = Regexp.escape(query) ref ||= root_ref - gitaly_repository_client.search_files_by_content(ref, safe_query) + gitaly_repository_client.search_files_by_content(ref, safe_query, options) end def can_be_merged?(source_sha, target_branch) diff --git a/lib/gitlab/git/rugged_impl/use_rugged.rb b/lib/gitlab/git/rugged_impl/use_rugged.rb index 068aaf03c51..f63e35030c1 100644 --- a/lib/gitlab/git/rugged_impl/use_rugged.rb +++ b/lib/gitlab/git/rugged_impl/use_rugged.rb @@ -16,7 +16,9 @@ module Gitlab end def running_puma_with_multiple_threads? - Gitlab::Runtime.puma? && ::Puma.cli_config.options[:max_threads] > 1 + return false unless Gitlab::Runtime.puma? + + ::Puma.respond_to?(:cli_config) && ::Puma.cli_config.options[:max_threads] > 1 end def execute_rugged_call(method_name, *args) diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 7e9ec097ef7..906350e57c5 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -50,8 +50,8 @@ module Gitlab @project = project @protocol = protocol @authentication_abilities = authentication_abilities - @namespace_path = namespace_path - @project_path = project_path + @namespace_path = namespace_path || project&.namespace&.full_path + @project_path = project_path || project&.path @redirected_path = redirected_path @auth_result_type = auth_result_type end @@ -60,6 +60,7 @@ module Gitlab @logger = Checks::TimedLogger.new(timeout: INTERNAL_TIMEOUT, header: LOG_HEADER) @changes = changes + check_namespace! check_protocol! check_valid_actor! check_active_user! @@ -136,6 +137,12 @@ module Gitlab end end + def check_namespace! + return if namespace_path.present? + + raise NotFoundError, ERROR_MESSAGES[:project_not_found] + end + def check_active_user! return unless user diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb new file mode 100644 index 00000000000..d99b9c3fe89 --- /dev/null +++ b/lib/gitlab/git_access_snippet.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Gitlab + class GitAccessSnippet < GitAccess + ERROR_MESSAGES = { + snippet_not_found: 'The snippet you were looking for could not be found.', + repository_not_found: 'The snippet repository you were looking for could not be found.' + }.freeze + + attr_reader :snippet + + def initialize(actor, snippet, protocol, **kwargs) + @snippet = snippet + + super(actor, project, protocol, **kwargs) + end + + def check(cmd, _changes) + unless Feature.enabled?(:version_snippets, user) + raise NotFoundError, ERROR_MESSAGES[:snippet_not_found] + end + + check_snippet_accessibility! + + success_result(cmd) + end + + def project + snippet&.project + end + + private + + def repository + snippet&.repository + end + + def check_snippet_accessibility! + if snippet.blank? + raise NotFoundError, ERROR_MESSAGES[:snippet_not_found] + end + + unless repository&.exists? + raise NotFoundError, ERROR_MESSAGES[:repository_not_found] + end + end + end +end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 262a1ef653f..4eb1ccf32ba 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -432,10 +432,7 @@ module Gitlab end def self.filesystem_id(storage) - response = Gitlab::GitalyClient::ServerService.new(storage).info - storage_status = response.storage_statuses.find { |status| status.storage_name == storage } - - storage_status&.filesystem_id + Gitlab::GitalyClient::ServerService.new(storage).storage_info&.filesystem_id end def self.filesystem_id_from_disk(storage) @@ -446,6 +443,14 @@ module Gitlab nil end + def self.filesystem_disk_available(storage) + Gitlab::GitalyClient::ServerService.new(storage).storage_disk_statistics&.available + end + + def self.filesystem_disk_used(storage) + Gitlab::GitalyClient::ServerService.new(storage).storage_disk_statistics&.used + end + def self.timeout(timeout_name) Gitlab::CurrentSettings.current_application_settings[timeout_name] end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 15318bc817a..ac22f5bf419 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -324,6 +324,7 @@ module Gitlab request.after = GitalyClient.timestamp(options[:after]) if options[:after] request.before = GitalyClient.timestamp(options[:before]) if options[:before] request.revision = encode_binary(options[:ref]) if options[:ref] + request.order = options[:order].upcase.sub('DEFAULT', 'NONE') if options[:order].present? request.paths = encode_repeated(Array(options[:path])) if options[:path].present? diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 27522f89a5b..67fb0ab9608 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -203,36 +203,6 @@ module Gitlab start_repository: start_repository) end - # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628 - def user_rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) - request = Gitaly::UserRebaseRequest.new( - repository: @gitaly_repo, - user: Gitlab::Git::User.from_gitlab(user).to_gitaly, - rebase_id: rebase_id.to_s, - branch: encode_binary(branch), - branch_sha: branch_sha, - remote_repository: remote_repository.gitaly_repository, - remote_branch: encode_binary(remote_branch) - ) - - response = GitalyClient.call( - @repository.storage, - :operation_service, - :user_rebase, - request, - timeout: GitalyClient.long_timeout, - remote_storage: remote_repository.storage - ) - - if response.pre_receive_error.presence - raise Gitlab::Git::PreReceiveError, response.pre_receive_error - elsif response.git_error.presence - raise Gitlab::Git::Repository::GitError, response.git_error - else - response.rebase_sha - end - end - def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:, push_options: []) request_enum = QueueEnumerator.new rebase_sha = nil diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index d1f848fae26..63def4e29c9 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -151,40 +151,6 @@ module Gitlab Gitlab::Git::Branch.new(@repository, encode!(branch.name.dup), branch.target_commit.id, target_commit) end - def create_branch(ref, start_point) - request = Gitaly::CreateBranchRequest.new( - repository: @gitaly_repo, - name: encode_binary(ref), - start_point: encode_binary(start_point) - ) - - response = GitalyClient.call(@repository.storage, :ref_service, :create_branch, request, timeout: GitalyClient.medium_timeout) - - case response.status - when :OK - branch = response.branch - target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit) - Gitlab::Git::Branch.new(@repository, branch.name, branch.target_commit.id, target_commit) - when :ERR_INVALID - invalid_ref!("Invalid ref name") - when :ERR_EXISTS - invalid_ref!("Branch #{ref} already exists") - when :ERR_INVALID_START_POINT - invalid_ref!("Invalid reference #{start_point}") - else - raise "Unknown response status: #{response.status}" - end - end - - def delete_branch(branch_name) - request = Gitaly::DeleteBranchRequest.new( - repository: @gitaly_repo, - name: encode_binary(branch_name) - ) - - GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request, timeout: GitalyClient.medium_timeout) - end - def delete_refs(refs: [], except_with_prefixes: []) request = Gitaly::DeleteRefsRequest.new( repository: @gitaly_repo, diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index d0e5e0db830..597ae4651ea 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -332,11 +332,11 @@ module Gitlab GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files) end - def search_files_by_content(ref, query) + def search_files_by_content(ref, query, options = {}) request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query) response = GitalyClient.call(@storage, :repository_service, :search_files_by_content, request, timeout: GitalyClient.default_timeout) - search_results_from_response(response) + search_results_from_response(response, options) end def disconnect_alternates @@ -361,18 +361,24 @@ module Gitlab private - def search_results_from_response(gitaly_response) + def search_results_from_response(gitaly_response, options = {}) + limit = options[:limit] + matches = [] + matches_count = 0 current_match = +"" gitaly_response.each do |message| next if message.nil? + break if limit && matches_count >= limit + current_match << message.match_data if message.end_of_match matches << current_match current_match = +"" + matches_count += 1 end end diff --git a/lib/gitlab/gitaly_client/server_service.rb b/lib/gitlab/gitaly_client/server_service.rb index 0ade6942db9..36bda67c26e 100644 --- a/lib/gitlab/gitaly_client/server_service.rb +++ b/lib/gitlab/gitaly_client/server_service.rb @@ -13,6 +13,24 @@ module Gitlab def info GitalyClient.call(@storage, :server_service, :server_info, Gitaly::ServerInfoRequest.new, timeout: GitalyClient.fast_timeout) end + + def disk_statistics + GitalyClient.call(@storage, :server_service, :disk_statistics, Gitaly::DiskStatisticsRequest.new, timeout: GitalyClient.fast_timeout) + end + + def storage_info + storage_specific(info) + end + + def storage_disk_statistics + storage_specific(disk_statistics) + end + + private + + def storage_specific(response) + response.storage_statuses.find { |status| status.storage_name == @storage } + end end end end diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb index 99bf4258c07..fcebcb463cd 100644 --- a/lib/gitlab/gl_repository.rb +++ b/lib/gitlab/gl_repository.rb @@ -7,17 +7,25 @@ module Gitlab PROJECT = RepoType.new( name: :project, access_checker_class: Gitlab::GitAccess, - repository_accessor: -> (project) { project.repository } + repository_resolver: -> (project) { project.repository } ).freeze WIKI = RepoType.new( name: :wiki, access_checker_class: Gitlab::GitAccessWiki, - repository_accessor: -> (project) { project.wiki.repository } + repository_resolver: -> (project) { project.wiki.repository }, + suffix: :wiki + ).freeze + SNIPPET = RepoType.new( + name: :snippet, + access_checker_class: Gitlab::GitAccessSnippet, + repository_resolver: -> (snippet) { snippet.repository }, + container_resolver: -> (id) { Snippet.find_by_id(id) } ).freeze TYPES = { PROJECT.name.to_s => PROJECT, - WIKI.name.to_s => WIKI + WIKI.name.to_s => WIKI, + SNIPPET.name.to_s => SNIPPET }.freeze def self.types @@ -27,15 +35,14 @@ module Gitlab def self.parse(gl_repository) type_name, _id = gl_repository.split('-').first type = types[type_name] - subject_id = type&.fetch_id(gl_repository) - unless subject_id + unless type raise ArgumentError, "Invalid GL Repository \"#{gl_repository}\"" end - project = Project.find_by_id(subject_id) + container = type.fetch_container!(gl_repository) - [project, type] + [container, type] end def self.default_type diff --git a/lib/gitlab/gl_repository/repo_type.rb b/lib/gitlab/gl_repository/repo_type.rb index 01bc27f963b..9663fd7de8f 100644 --- a/lib/gitlab/gl_repository/repo_type.rb +++ b/lib/gitlab/gl_repository/repo_type.rb @@ -5,16 +5,25 @@ module Gitlab class RepoType attr_reader :name, :access_checker_class, - :repository_accessor + :repository_resolver, + :container_resolver, + :suffix - def initialize(name:, access_checker_class:, repository_accessor:) + def initialize( + name:, + access_checker_class:, + repository_resolver:, + container_resolver: default_container_resolver, + suffix: nil) @name = name @access_checker_class = access_checker_class - @repository_accessor = repository_accessor + @repository_resolver = repository_resolver + @container_resolver = container_resolver + @suffix = suffix end - def identifier_for_subject(subject) - "#{name}-#{subject.id}" + def identifier_for_container(container) + "#{name}-#{container.id}" end def fetch_id(identifier) @@ -22,6 +31,14 @@ module Gitlab match[:id] if match end + def fetch_container!(identifier) + id = fetch_id(identifier) + + raise ArgumentError, "Invalid GL Repository \"#{identifier}\"" unless id + + container_resolver.call(id) + end + def wiki? self == WIKI end @@ -30,12 +47,26 @@ module Gitlab self == PROJECT end + def snippet? + self == SNIPPET + end + def path_suffix - project? ? "" : ".#{name}" + suffix ? ".#{suffix}" : '' end - def repository_for(subject) - repository_accessor.call(subject) + def repository_for(container) + repository_resolver.call(container) + end + + def valid?(repository_path) + repository_path.end_with?(path_suffix) + end + + private + + def default_container_resolver + -> (id) { Project.find_by_id(id) } end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 2e27e954e79..3db6c3b51c0 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -43,6 +43,9 @@ module Gitlab # Initialize gon.features with any flags that should be # made globally available to the frontend push_frontend_feature_flag(:snippets_vue, default_enabled: false) + push_frontend_feature_flag(:monaco_snippets, default_enabled: false) + push_frontend_feature_flag(:monaco_blobs, default_enabled: false) + push_frontend_feature_flag(:monaco_ci, default_enabled: false) end # Exposes the state of a feature flag to the frontend code. diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 7e6f6a519a6..8166bef4510 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -6,7 +6,7 @@ module Gitlab CleanupError = Class.new(StandardError) BG_CLEANUP_RUNTIME_S = 10 - FG_CLEANUP_RUNTIME_S = 0.5 + FG_CLEANUP_RUNTIME_S = 1 MUTEX = Mutex.new @@ -127,7 +127,10 @@ module Gitlab # error. # Failing to remove the tmp directory could leave the `gpg-agent` process # running forever. - Retriable.retriable(max_elapsed_time: cleanup_time, base_interval: 0.1) do + # + # 15 tries will never complete within the maximum time with exponential + # backoff. So our limit is the runtime, not the number of tries. + Retriable.retriable(max_elapsed_time: cleanup_time, base_interval: 0.1, tries: 15) do FileUtils.remove_entry(tmp_dir) if File.exist?(tmp_dir) end rescue => e diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index dc71d0b427a..1abbd6dc45b 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -2,36 +2,9 @@ module Gitlab module Gpg - class Commit - include Gitlab::Utils::StrongMemoize - - def initialize(commit) - @commit = commit - - repo = commit.project.repository.raw_repository - @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id) - - lazy_signature - end - - def signature_text - strong_memoize(:signature_text) do - @signature_data&.itself && @signature_data[0] - end - end - - def signed_text - strong_memoize(:signed_text) do - @signature_data&.itself && @signature_data[1] - end - end - - def has_signature? - !!(signature_text && signed_text) - end - + class Commit < Gitlab::SignedCommit def signature - return unless has_signature? + super return @signature if @signature diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb index 9bb1e8fc7a2..837473d47cd 100644 --- a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb +++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb @@ -25,9 +25,12 @@ module Gitlab def process_params(data) return [] unless data.has_key?(:params) - data[:params] - .each_pair - .map { |k, v| { key: k, value: utf8_encode_values(v) } } + params_array = + data[:params] + .each_pair + .map { |k, v| { key: k, value: utf8_encode_values(v) } } + + Gitlab::Utils::LogLimitedArray.log_limited_array(params_array) end def utf8_encode_values(data) diff --git a/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb index 22728cc0b65..26c9d77a8df 100644 --- a/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb +++ b/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb @@ -6,8 +6,16 @@ module Gitlab module Keyset module Conditions class BaseCondition - def initialize(arel_table, names, values, operator, before_or_after) - @arel_table, @names, @values, @operator, @before_or_after = arel_table, names, values, operator, before_or_after + # @param [Arel::Table] arel_table for the relation being ordered + # @param [Array<OrderInfo>] order_list of extracted orderings + # @param [Array] values from the decoded cursor + # @param [Array<String>] operators determining sort comparison + # @param [Symbol] before_or_after indicates whether we want + # items :before the cursor or :after the cursor + def initialize(arel_table, order_list, values, operators, before_or_after) + @arel_table, @order_list, @values, @operators, @before_or_after = arel_table, order_list, values, operators, before_or_after + + @before_or_after = :after unless [:after, :before].include?(@before_or_after) end def build @@ -16,20 +24,27 @@ module Gitlab private - attr_reader :arel_table, :names, :values, :operator, :before_or_after + attr_reader :arel_table, :order_list, :values, :operators, :before_or_after + + 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' + else + target = arel_table[order_info.attribute_name] + end - def table_condition(attribute, value, operator) case operator when '>' - arel_table[attribute].gt(value) + target.gt(value) when '<' - arel_table[attribute].lt(value) + target.lt(value) when '=' - arel_table[attribute].eq(value) + target.eq(value) when 'is_null' - arel_table[attribute].eq(nil) + target.eq(nil) when 'is_not_null' - arel_table[attribute].not_eq(nil) + target.not_eq(nil) end end end diff --git a/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb index 3b56ddb996d..3239d27c0cd 100644 --- a/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb +++ b/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb @@ -12,7 +12,7 @@ module Gitlab # If there is only one order field, we can assume it # does not contain NULLs, and don't need additional # conditions - unless names.count == 1 + unless order_list.count == 1 conditions << [second_attribute_condition, final_condition] end @@ -24,7 +24,7 @@ module Gitlab # ex: "(relative_position > 23)" def first_attribute_condition <<~SQL - (#{table_condition(names.first, values.first, operator.first).to_sql}) + (#{table_condition(order_list.first, values.first, operators.first).to_sql}) SQL end @@ -32,9 +32,9 @@ module Gitlab def second_attribute_condition condition = <<~SQL OR ( - #{table_condition(names.first, values.first, '=').to_sql} + #{table_condition(order_list.first, values.first, '=').to_sql} AND - #{table_condition(names[1], values[1], operator[1]).to_sql} + #{table_condition(order_list[1], values[1], operators[1]).to_sql} ) SQL @@ -45,7 +45,7 @@ module Gitlab def final_condition if before_or_after == :after <<~SQL - OR (#{table_condition(names.first, nil, 'is_null').to_sql}) + OR (#{table_condition(order_list.first, nil, 'is_null').to_sql}) SQL end end diff --git a/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb index 71a74936d5d..18ea0692e2c 100644 --- a/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb +++ b/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb @@ -16,9 +16,9 @@ module Gitlab def first_attribute_condition condition = <<~SQL ( - #{table_condition(names.first, nil, 'is_null').to_sql} + #{table_condition(order_list.first, nil, 'is_null').to_sql} AND - #{table_condition(names[1], values[1], operator[1]).to_sql} + #{table_condition(order_list[1], values[1], operators[1]).to_sql} ) SQL @@ -29,7 +29,7 @@ module Gitlab def final_condition if before_or_after == :before <<~SQL - OR (#{table_condition(names.first, nil, 'is_not_null').to_sql}) + OR (#{table_condition(order_list.first, nil, 'is_not_null').to_sql}) SQL end end diff --git a/lib/gitlab/graphql/connections/keyset/order_info.rb b/lib/gitlab/graphql/connections/keyset/order_info.rb index 4d85e8f79b7..7f61bf937b4 100644 --- a/lib/gitlab/graphql/connections/keyset/order_info.rb +++ b/lib/gitlab/graphql/connections/keyset/order_info.rb @@ -5,15 +5,15 @@ module Gitlab module Connections module Keyset class OrderInfo - attr_reader :attribute_name, :sort_direction + attr_reader :attribute_name, :sort_direction, :named_function def initialize(order_value) - if order_value.is_a?(String) - @attribute_name, @sort_direction = extract_nulls_last_order(order_value) - else - @attribute_name = order_value.expr.name - @sort_direction = order_value.direction - end + @attribute_name, @sort_direction, @named_function = + if order_value.is_a?(String) + extract_nulls_last_order(order_value) + else + extract_attribute_values(order_value) + end end def operator_for(before_or_after) @@ -69,7 +69,24 @@ module Gitlab def extract_nulls_last_order(order_value) tokens = order_value.downcase.split - [tokens.first, (tokens[1] == 'asc' ? :asc : :desc)] + [tokens.first, (tokens[1] == 'asc' ? :asc : :desc), nil] + 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] + 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 end end diff --git a/lib/gitlab/graphql/connections/keyset/query_builder.rb b/lib/gitlab/graphql/connections/keyset/query_builder.rb index e93c25d85fc..fe85898f638 100644 --- a/lib/gitlab/graphql/connections/keyset/query_builder.rb +++ b/lib/gitlab/graphql/connections/keyset/query_builder.rb @@ -40,17 +40,16 @@ module Gitlab # "issues"."id" > 500 # def conditions - attr_names = order_list.map { |field| field.attribute_name } - attr_values = attr_names.map { |name| decoded_cursor[name] } + attr_values = order_list.map { |field| decoded_cursor[field.attribute_name] } - if attr_names.count == 1 && attr_values.first.nil? + if order_list.count == 1 && attr_values.first.nil? raise Gitlab::Graphql::Errors::ArgumentError.new('Before/after cursor invalid: `nil` was provided as only sortable value') end - if attr_names.count == 1 || attr_values.first.present? - Keyset::Conditions::NotNullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build + if order_list.count == 1 || attr_values.first.present? + Keyset::Conditions::NotNullCondition.new(arel_table, order_list, attr_values, operators, before_or_after).build else - Keyset::Conditions::NullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build + Keyset::Conditions::NullCondition.new(arel_table, order_list, attr_values, operators, before_or_after).build end end diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb index ac2a78c0f28..56524120ffd 100644 --- a/lib/gitlab/graphql/docs/helper.rb +++ b/lib/gitlab/graphql/docs/helper.rb @@ -21,6 +21,10 @@ module Gitlab MD end + def sorted_fields(fields) + fields.sort_by { |field| field[:name] } + end + # Some fields types are arrays of other types and are displayed # on docs wrapped in square brackets, for example: [String!]. # This makes GitLab docs renderer thinks they are links so here diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml index 52568286dca..b126a22c301 100644 --- a/lib/gitlab/graphql/docs/templates/default.md.haml +++ b/lib/gitlab/graphql/docs/templates/default.md.haml @@ -9,8 +9,8 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graphiql). -Each table below documents a GraphQL type. Types match loosely to models, but not all -fields and methods on a model are available via GraphQL. + Each table below documents a GraphQL type. Types match loosely to models, but not all + fields and methods on a model are available via GraphQL. \ - objects.each do |type| - unless type[:fields].empty? @@ -21,6 +21,6 @@ fields and methods on a model are available via GraphQL. \ ~ "| Name | Type | Description |" ~ "| --- | ---- | ---------- |" - - type[:fields].each do |field| + - sorted_fields(type[:fields]).each do |field| = "| `#{field[:name]}` | #{render_field_type(field[:type][:info])} | #{field[:description]} |" \ diff --git a/lib/gitlab/graphql/extensions/externally_paginated_array_extension.rb b/lib/gitlab/graphql/extensions/externally_paginated_array_extension.rb new file mode 100644 index 00000000000..1adedb500e6 --- /dev/null +++ b/lib/gitlab/graphql/extensions/externally_paginated_array_extension.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module Gitlab + module Graphql + module Extensions + class ExternallyPaginatedArrayExtension < GraphQL::Schema::Field::ConnectionExtension + def resolve(object:, arguments:, context:) + yield(object, arguments) + end + end + end + end +end diff --git a/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb b/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb index ccf9e597307..79a7104a2ff 100644 --- a/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb +++ b/lib/gitlab/graphql/query_analyzers/recursion_analyzer.rb @@ -6,7 +6,7 @@ module Gitlab module Graphql module QueryAnalyzers class RecursionAnalyzer - IGNORED_FIELDS = %w(node edges ofType).freeze + IGNORED_FIELDS = %w(node edges nodes ofType).freeze RECURSION_THRESHOLD = 2 def initial_value(query) diff --git a/lib/gitlab/health_checks/base_abstract_check.rb b/lib/gitlab/health_checks/base_abstract_check.rb index 199cd2f9b2d..7e1c5331b07 100644 --- a/lib/gitlab/health_checks/base_abstract_check.rb +++ b/lib/gitlab/health_checks/base_abstract_check.rb @@ -32,11 +32,9 @@ module Gitlab end def catch_timeout(seconds, &block) - begin - Timeout.timeout(seconds.to_i, &block) - rescue Timeout::Error => ex - ex - end + Timeout.timeout(seconds.to_i, &block) + rescue Timeout::Error => ex + ex end end end diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 22b9a038768..07c43fa4832 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -32,7 +32,7 @@ module Gitlab @lexer ||= custom_language || begin Rouge::Lexer.guess(filename: @blob_name, source: @blob_content).new rescue Rouge::Guesser::Ambiguous => e - e.alternatives.sort_by(&:tag).first + e.alternatives.min_by(&:tag) end end diff --git a/lib/gitlab/import_export/base_relation_factory.rb b/lib/gitlab/import_export/base_relation_factory.rb index 562b549f6a1..d3c8802bcce 100644 --- a/lib/gitlab/import_export/base_relation_factory.rb +++ b/lib/gitlab/import_export/base_relation_factory.rb @@ -24,7 +24,8 @@ module Gitlab last_edited_by_id merge_user_id resolved_by_id - closed_by_id owner_id + closed_by_id + owner_id ].freeze TOKEN_RESET_MODELS = %i[Project Namespace Group Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze diff --git a/lib/gitlab/import_export/group_import_export.yml b/lib/gitlab/import_export/group_import_export.yml index 049d81f96a4..d4e0ff12373 100644 --- a/lib/gitlab/import_export/group_import_export.yml +++ b/lib/gitlab/import_export/group_import_export.yml @@ -8,10 +8,14 @@ tree: - :milestones - :badges - labels: - - :priorities - - :boards + - :priorities + - boards: + - lists: + - label: + - :priorities + - :board - members: - - :user + - :user included_attributes: user: @@ -24,16 +28,28 @@ included_attributes: excluded_attributes: group: - :id + - :owner_id + - :parent_id + - :created_at + - :updated_at - :runners_token - :runners_token_encrypted + - :saml_discovery_token + - :visibility_level methods: labels: - :type + label: + - :type badges: - :type notes: - :type + events: + - :action + lists: + - :list_type preloads: @@ -43,10 +59,20 @@ ee: tree: group: - epics: - - :parent - - notes: - - :author + - :parent + - :award_emoji + - events: + - :push_event_payload + - notes: + - :author + - :award_emoji + - events: + - :push_event_payload - boards: - - :board_assignee - - labels: - - :priorities + - :board_assignee + - labels: + - :priorities + - lists: + - milestone: + - events: + - :push_event_payload diff --git a/lib/gitlab/import_export/group_object_builder.rb b/lib/gitlab/import_export/group_object_builder.rb new file mode 100644 index 00000000000..9796bfa07d4 --- /dev/null +++ b/lib/gitlab/import_export/group_object_builder.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + # Given a class, it finds or creates a new object at group level. + # + # Example: + # `GroupObjectBuilder.build(Label, label_attributes)` + # finds or initializes a label with the given attributes. + class GroupObjectBuilder < BaseObjectBuilder + def self.build(*args) + Group.transaction do + super + end + end + + def initialize(klass, attributes) + super + + @group = @attributes['group'] + + update_description + end + + private + + attr_reader :group + + # Convert description empty string to nil + # due to existing object being saved with description: nil + # Which makes object lookup to fail since nil != '' + def update_description + attributes['description'] = nil if attributes['description'] == '' + end + + def where_clauses + [ + where_clause_base, + where_clause_for_title, + where_clause_for_description, + where_clause_for_created_at + ].compact + end + + # Returns Arel clause `"{table_name}"."group_id" = {group.id}` + def where_clause_base + table[:group_id].in(group_and_ancestor_ids) + end + + def group_and_ancestor_ids + group.ancestors.map(&:id) << group.id + end + end + end +end diff --git a/lib/gitlab/import_export/group_project_object_builder.rb b/lib/gitlab/import_export/group_project_object_builder.rb index d6d780f165e..9e8f9d11393 100644 --- a/lib/gitlab/import_export/group_project_object_builder.rb +++ b/lib/gitlab/import_export/group_project_object_builder.rb @@ -50,7 +50,7 @@ module Gitlab def where_clause_base [].tap do |clauses| clauses << table[:project_id].eq(project.id) if project - clauses << table[:group_id].eq(group.id) if group + clauses << table[:group_id].in(group.self_and_ancestors_ids) if group end.reduce(:or) end @@ -60,7 +60,9 @@ module Gitlab end def prepare_attributes - attributes.except('group').tap do |atts| + attributes.dup.tap do |atts| + atts.delete('group') unless epic? + if label? atts['type'] = 'ProjectLabel' # Always create project labels elsif milestone? diff --git a/lib/gitlab/import_export/group_relation_factory.rb b/lib/gitlab/import_export/group_relation_factory.rb new file mode 100644 index 00000000000..e3597af44d2 --- /dev/null +++ b/lib/gitlab/import_export/group_relation_factory.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + class GroupRelationFactory < BaseRelationFactory + OVERRIDES = { + labels: :group_labels, + priorities: :label_priorities, + label: :group_label, + parent: :epic + }.freeze + + EXISTING_OBJECT_RELATIONS = %i[ + epic + epics + milestone + milestones + label + labels + group_label + group_labels + ].freeze + + private + + def setup_models + setup_note if @relation_name == :notes + + update_group_references + end + + def update_group_references + return unless self.class.existing_object_relations.include?(@relation_name) + return unless @relation_hash['group_id'] + + @relation_hash['group_id'] = @importable.id + end + end + end +end diff --git a/lib/gitlab/import_export/group_tree_restorer.rb b/lib/gitlab/import_export/group_tree_restorer.rb new file mode 100644 index 00000000000..2f42843ed6c --- /dev/null +++ b/lib/gitlab/import_export/group_tree_restorer.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + class GroupTreeRestorer + attr_reader :user + attr_reader :shared + attr_reader :group + + def initialize(user:, shared:, group:, group_hash:) + @path = File.join(shared.export_path, 'group.json') + @user = user + @shared = shared + @group = group + @group_hash = group_hash + end + + def restore + @tree_hash = @group_hash || read_tree_hash + @group_members = @tree_hash.delete('members') + @children = @tree_hash.delete('children') + + if members_mapper.map && restorer.restore + @children&.each do |group_hash| + group = create_group(group_hash: group_hash, parent_group: @group) + shared = Gitlab::ImportExport::Shared.new(group) + + self.class.new( + user: @user, + shared: shared, + group: group, + group_hash: group_hash + ).restore + end + end + + return false if @shared.errors.any? + + true + rescue => e + @shared.error(e) + false + end + + private + + def read_tree_hash + json = IO.read(@path) + ActiveSupport::JSON.decode(json) + rescue => e + @shared.logger.error( + group_id: @group.id, + group_name: @group.name, + message: "Import/Export error: #{e.message}" + ) + + raise Gitlab::ImportExport::Error.new('Incorrect JSON format') + end + + def restorer + @relation_tree_restorer ||= RelationTreeRestorer.new( + user: @user, + shared: @shared, + importable: @group, + tree_hash: @tree_hash.except('name', 'path'), + members_mapper: members_mapper, + object_builder: object_builder, + relation_factory: relation_factory, + reader: reader + ) + end + + def create_group(group_hash:, parent_group:) + group_params = { + name: group_hash['name'], + path: group_hash['path'], + parent_id: parent_group&.id, + visibility_level: sub_group_visibility_level(group_hash, parent_group) + } + + ::Groups::CreateService.new(@user, group_params).execute + end + + def sub_group_visibility_level(group_hash, parent_group) + original_visibility_level = group_hash['visibility_level'] || Gitlab::VisibilityLevel::PRIVATE + + if parent_group && parent_group.visibility_level < original_visibility_level + Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level) + else + original_visibility_level + end + end + + def members_mapper + @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @group_members, user: @user, importable: @group) + end + + def relation_factory + Gitlab::ImportExport::GroupRelationFactory + end + + def object_builder + Gitlab::ImportExport::GroupObjectBuilder + end + + def reader + @reader ||= Gitlab::ImportExport::Reader.new( + shared: @shared, + config: Gitlab::ImportExport::Config.new( + config: Gitlab::ImportExport.group_config_file + ).to_h + ) + end + end + end +end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 2acb79e3e22..e55ad898263 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -25,8 +25,6 @@ tree: - milestone: - events: - :push_event_payload - - issue_milestones: - - :milestone - resource_label_events: - label: - :priorities @@ -64,8 +62,6 @@ tree: - milestone: - events: - :push_event_payload - - merge_request_milestones: - - :milestone - resource_label_events: - label: - :priorities @@ -178,7 +174,6 @@ excluded_attributes: - :encrypted_secret_token - :encrypted_secret_token_iv - :repository_storage - - :storage_version merge_request_diff: - :external_diff - :stored_externally @@ -213,12 +208,6 @@ excluded_attributes: - :latest_merge_request_diff_id - :head_pipeline_id - :state_id - issue_milestones: - - :milestone_id - - :issue_id - merge_request_milestones: - - :milestone_id - - :merge_request_id award_emoji: - :awardable_id statuses: diff --git a/lib/gitlab/import_export/import_failure_service.rb b/lib/gitlab/import_export/import_failure_service.rb index eeaf10870c8..d4eca551b49 100644 --- a/lib/gitlab/import_export/import_failure_service.rb +++ b/lib/gitlab/import_export/import_failure_service.rb @@ -12,9 +12,14 @@ module Gitlab @association = importable.association(:import_failures) end - def with_retry(relation_key, relation_index) + def with_retry(action:, relation_key: nil, relation_index: nil) on_retry = -> (exception, retry_count, *_args) do - log_import_failure(relation_key, relation_index, exception, retry_count) + log_import_failure( + source: action, + relation_key: relation_key, + relation_index: relation_index, + exception: exception, + retry_count: retry_count) end Retriable.with_context(:relation_import, on_retry: on_retry) do @@ -22,8 +27,9 @@ module Gitlab end end - def log_import_failure(relation_key, relation_index, exception, retry_count = 0) + def log_import_failure(source:, relation_key: nil, relation_index: nil, exception:, retry_count: 0) extra = { + source: source, relation_key: relation_key, relation_index: relation_index, retry_count: retry_count diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index d2e27388b51..2a70344374b 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -9,7 +9,7 @@ module Gitlab @importable = importable # This needs to run first, as second call would be from #map - # which means project members already exist. + # which means Project/Group members already exist. ensure_default_member! end @@ -47,6 +47,8 @@ module Gitlab end def ensure_default_member! + return if user_already_member? + @importable.members.destroy_all # rubocop: disable DestroyAll relation_class.create!(user: @user, access_level: relation_class::MAINTAINER, source_id: @importable.id, importing: true) @@ -54,6 +56,12 @@ module Gitlab raise e, "Error adding importer user to #{@importable.class} members. #{e.message}" end + def user_already_member? + member = @importable.members&.first + + member&.user == @user && member.access_level >= relation_class::MAINTAINER + end + def add_team_member(member, existing_user = nil) member['user'] = existing_user @@ -74,7 +82,7 @@ module Gitlab end def find_user_query(member) - user_arel[:email].eq(member['user']['email']).or(user_arel[:username].eq(member['user']['username'])) + user_arel[:email].eq(member['user']['email']) end def user_arel diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb index 0b534a5bafc..f735b9612aa 100644 --- a/lib/gitlab/import_export/merge_request_parser.rb +++ b/lib/gitlab/import_export/merge_request_parser.rb @@ -16,7 +16,7 @@ module Gitlab if fork_merge_request? && @diff_head_sha @merge_request.source_project_id = @relation_hash['project_id'] - fetch_ref unless branch_exists?(@merge_request.source_branch) + create_source_branch unless branch_exists?(@merge_request.source_branch) create_target_branch unless branch_exists?(@merge_request.target_branch) end @@ -34,17 +34,18 @@ module Gitlab @merge_request end - def create_target_branch - @project.repository.create_branch(@merge_request.target_branch, @merge_request.target_branch_sha) + # When the exported MR was in a fork, the source branch does not exist in + # the imported bundle - although the commits usually do - so it must be + # created manually. Ignore failures so we get the merge request itself if + # the commits are missing. + def create_source_branch + @project.repository.create_branch(@merge_request.source_branch, @diff_head_sha) + rescue => err + Rails.logger.warn("Import/Export warning: Failed to create source branch #{@merge_request.source_branch} => #{@diff_head_sha} for MR #{@merge_request.iid}: #{err}") # rubocop:disable Gitlab/RailsLogger end - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1295 - def fetch_ref - target_ref = Gitlab::Git::BRANCH_REF_PREFIX + @merge_request.source_branch - - unless @project.repository.fetch_source_branch!(@project.repository, @diff_head_sha, target_ref) - Rails.logger.warn("Import/Export warning: Failed to create #{target_ref} for MR: #{@merge_request.iid}") # rubocop:disable Gitlab/RailsLogger - end + def create_target_branch + @project.repository.create_branch(@merge_request.target_branch, @merge_request.target_branch_sha) end def branch_exists?(branch_name) diff --git a/lib/gitlab/import_export/project_tree_loader.rb b/lib/gitlab/import_export/project_tree_loader.rb new file mode 100644 index 00000000000..fc21858043d --- /dev/null +++ b/lib/gitlab/import_export/project_tree_loader.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + class ProjectTreeLoader + def load(path, dedup_entries: false) + tree_hash = ActiveSupport::JSON.decode(IO.read(path)) + + if dedup_entries + dedup_tree(tree_hash) + else + tree_hash + end + end + + private + + # This function removes duplicate entries from the given tree recursively + # by caching nodes it encounters repeatedly. We only consider nodes for + # which there can actually be multiple equivalent instances (e.g. strings, + # hashes and arrays, but not `nil`s, numbers or booleans.) + # + # The algorithm uses a recursive depth-first descent with 3 cases, starting + # with a root node (the tree/hash itself): + # - a node has already been cached; in this case we return it from the cache + # - a node has not been cached yet but should be; descend into its children + # - a node is neither cached nor qualifies for caching; this is a no-op + def dedup_tree(node, nodes_seen = {}) + if nodes_seen.key?(node) && distinguishable?(node) + yield nodes_seen[node] + elsif should_dedup?(node) + nodes_seen[node] = node + + case node + when Array + node.each_index do |idx| + dedup_tree(node[idx], nodes_seen) do |cached_node| + node[idx] = cached_node + end + end + when Hash + node.each do |k, v| + dedup_tree(v, nodes_seen) do |cached_node| + node[k] = cached_node + end + end + end + else + node + end + end + + # We do not need to consider nodes for which there cannot be multiple instances + def should_dedup?(node) + node && !(node.is_a?(Numeric) || node.is_a?(TrueClass) || node.is_a?(FalseClass)) + end + + # We can only safely de-dup values that are distinguishable. True value objects + # are always distinguishable by nature. Hashes however can represent entities, + # which are identified by ID, not value. We therefore disallow de-duping hashes + # that do not have an `id` field, since we might risk dropping entities that + # have equal attributes yet different identities. + def distinguishable?(node) + if node.is_a?(Hash) + node.key?('id') + else + true + end + end + end + end +end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index e598cfc143e..aae07657ea0 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -3,15 +3,17 @@ module Gitlab module ImportExport class ProjectTreeRestorer + LARGE_PROJECT_FILE_SIZE_BYTES = 500.megabyte + attr_reader :user attr_reader :shared attr_reader :project def initialize(user:, shared:, project:) - @path = File.join(shared.export_path, 'project.json') @user = user @shared = shared @project = project + @tree_loader = ProjectTreeLoader.new end def restore @@ -21,7 +23,9 @@ module Gitlab RelationRenameService.rename(@tree_hash) if relation_tree_restorer.restore - @project.merge_requests.set_latest_merge_request_diff_ids! + import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do + @project.merge_requests.set_latest_merge_request_diff_ids! + end true else @@ -34,9 +38,16 @@ module Gitlab private + def large_project?(path) + File.size(path) >= LARGE_PROJECT_FILE_SIZE_BYTES + end + def read_tree_hash - json = IO.read(@path) - ActiveSupport::JSON.decode(json) + path = File.join(@shared.export_path, 'project.json') + dedup_entries = large_project?(path) && + Feature.enabled?(:dedup_project_import_metadata, project.group) + + @tree_loader.load(path, dedup_entries: dedup_entries) rescue => e Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger raise Gitlab::ImportExport::Error.new('Incorrect JSON format') @@ -72,6 +83,10 @@ module Gitlab def reader @reader ||= Gitlab::ImportExport::Reader.new(shared: @shared) end + + def import_failure_service + @import_failure_service ||= ImportFailureService.new(@project) + end end end end diff --git a/lib/gitlab/import_export/relation_tree_restorer.rb b/lib/gitlab/import_export/relation_tree_restorer.rb index 44cf90fb86a..cc01d70db16 100644 --- a/lib/gitlab/import_export/relation_tree_restorer.rb +++ b/lib/gitlab/import_export/relation_tree_restorer.rb @@ -73,13 +73,17 @@ module Gitlab relation_object.assign_attributes(importable_class_sym => @importable) - import_failure_service.with_retry(relation_key, relation_index) do + import_failure_service.with_retry(action: 'relation_object.save!', relation_key: relation_key, relation_index: relation_index) do relation_object.save! end save_id_mapping(relation_key, data_hash, relation_object) rescue => e - import_failure_service.log_import_failure(relation_key, relation_index, e) + import_failure_service.log_import_failure( + source: 'process_relation_item!', + relation_key: relation_key, + relation_index: relation_index, + exception: e) end def import_failure_service @@ -155,7 +159,7 @@ module Gitlab def build_relation(relation_key, relation_definition, data_hash) # TODO: This is hack to not create relation for the author # Rather make `RelationFactory#set_note_author` to take care of that - return data_hash if relation_key == 'author' + return data_hash if relation_key == 'author' || already_restored?(data_hash) # create relation objects recursively for all sub-objects relation_definition.each do |sub_relation_key, sub_relation_definition| @@ -165,6 +169,13 @@ module Gitlab @relation_factory.create(relation_factory_params(relation_key, data_hash)) end + # Since we update the data hash in place as we restore relation items, + # and since we also de-duplicate items, we might encounter items that + # have already been restored in a previous iteration. + def already_restored?(relation_item) + !relation_item.is_a?(Hash) + end + def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition) sub_data_hash = data_hash[sub_relation_key] return unless sub_data_hash diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index eece4edf895..4547a9b0a01 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -61,7 +61,7 @@ module Gitlab regex = Regexp.escape(wildcard_address) regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)') - Regexp.new(/\A#{regex}\z/).freeze + Regexp.new(/\A<?#{regex}>?\z/).freeze end end end diff --git a/lib/gitlab/jira/http_client.rb b/lib/gitlab/jira/http_client.rb index 0c8b509740c..c09d8170d17 100644 --- a/lib/gitlab/jira/http_client.rb +++ b/lib/gitlab/jira/http_client.rb @@ -12,7 +12,7 @@ module Gitlab def request(*args) result = make_request(*args) - raise JIRA::HTTPError.new(result) unless result.response.is_a?(Net::HTTPSuccess) + raise JIRA::HTTPError.new(result.response) unless result.response.is_a?(Net::HTTPSuccess) result end diff --git a/lib/gitlab/kubernetes/generic_secret.rb b/lib/gitlab/kubernetes/generic_secret.rb new file mode 100644 index 00000000000..45adf869da0 --- /dev/null +++ b/lib/gitlab/kubernetes/generic_secret.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + class GenericSecret + attr_reader :name, :data, :namespace_name + + def initialize(name, data, namespace_name) + @name = name + @data = data + @namespace_name = namespace_name + end + + def generate + ::Kubeclient::Resource.new( + type: generic_secret_type, + metadata: metadata, + data: data + ) + end + + private + + def generic_secret_type + 'Opaque' + end + + def metadata + { + name: name, + namespace: namespace_name + } + end + end + end +end diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb index 7cb7f46a623..7c5525b982c 100644 --- a/lib/gitlab/kubernetes/kube_client.rb +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -16,6 +16,7 @@ module Gitlab SUPPORTED_API_GROUPS = { core: { group: 'api', version: 'v1' }, rbac: { group: 'apis/rbac.authorization.k8s.io', version: 'v1' }, + apps: { group: 'apis/apps', version: 'v1' }, extensions: { group: 'apis/extensions', version: 'v1beta1' }, istio: { group: 'apis/networking.istio.io', version: 'v1alpha3' }, knative: { group: 'apis/serving.knative.dev', version: 'v1alpha1' } @@ -74,10 +75,6 @@ module Gitlab :update_role_binding, to: :rbac_client - # Deployments resource is currently on the apis/extensions api group - delegate :get_deployments, - to: :extensions_client - # non-entity methods that can only work with the core client # as it uses the pods/log resource delegate :get_pod_log, @@ -93,16 +90,40 @@ module Gitlab attr_reader :api_prefix, :kubeclient_options + DEFAULT_KUBECLIENT_OPTIONS = { + timeouts: { + open: 10, + read: 30 + } + }.freeze + # We disable redirects through 'http_max_redirects: 0', # so that KubeClient does not follow redirects and # expose internal services. def initialize(api_prefix, **kubeclient_options) @api_prefix = api_prefix - @kubeclient_options = kubeclient_options.merge(http_max_redirects: 0) + @kubeclient_options = DEFAULT_KUBECLIENT_OPTIONS + .deep_merge(kubeclient_options) + .merge(http_max_redirects: 0) validate_url! end + # Deployments resource is currently on the apis/extensions api group + # until Kubernetes 1.15. Kubernetest 1.16+ has deployments resources in + # the apis/apps api group. + # + # As we still support Kubernetes 1.12+, we will need to support both. + def get_deployments(**args) + extensions_client.discover unless extensions_client.discovered + + if extensions_client.respond_to?(:get_deployments) + extensions_client.get_deployments(**args) + else + apps_client.get_deployments(**args) + end + end + def create_or_update_cluster_role_binding(resource) if cluster_role_binding_exists?(resource) update_cluster_role_binding(resource) diff --git a/lib/gitlab/kubernetes/tls_secret.rb b/lib/gitlab/kubernetes/tls_secret.rb new file mode 100644 index 00000000000..2895f4df27c --- /dev/null +++ b/lib/gitlab/kubernetes/tls_secret.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + class TlsSecret + attr_reader :name, :cert, :key, :namespace_name + + def initialize(name, cert, key, namespace_name) + @name = name + @cert = cert + @key = key + @namespace_name = namespace_name + end + + def generate + ::Kubeclient::Resource.new( + type: tls_secret_type, + metadata: metadata, + data: data + ) + end + + private + + def tls_secret_type + 'kubernetes.io/tls' + end + + def metadata + { + name: name, + namespace: namespace_name + } + end + + def data + { + 'tls.crt': Base64.strict_encode64(cert), + 'tls.key': Base64.strict_encode64(key) + } + end + end + end +end diff --git a/lib/gitlab/log_timestamp_formatter.rb b/lib/gitlab/log_timestamp_formatter.rb new file mode 100644 index 00000000000..433dedeb7a0 --- /dev/null +++ b/lib/gitlab/log_timestamp_formatter.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Gitlab + class LogTimestampFormatter < Logger::Formatter + FORMAT = "%s, [%s #%d] %5s -- %s: %s\n" + + def call(severity, timestamp, program_name, message) + FORMAT % [severity[0..0], timestamp.utc.iso8601(3), $$, severity, program_name, msg2str(message)] + end + end +end diff --git a/lib/gitlab/looping_batcher.rb b/lib/gitlab/looping_batcher.rb new file mode 100644 index 00000000000..adf0aeda506 --- /dev/null +++ b/lib/gitlab/looping_batcher.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module Gitlab + # Returns an ID range within a table so it can be iterated over. Repeats from + # the beginning after it reaches the end. + # + # Used by Geo in particular to iterate over a replicable and its registry + # table. + # + # Tracks a cursor for each table, by "key". If the table is smaller than + # batch_size, then a range for the whole table is returned on every call. + class LoopingBatcher + # @param [Class] model_class the class of the table to iterate on + # @param [String] key to identify the cursor. Note, cursor is already unique + # per table. + # @param [Integer] batch_size to limit the number of records in a batch + def initialize(model_class, key:, batch_size: 1000) + @model_class = model_class + @key = key + @batch_size = batch_size + end + + # @return [Range] a range of IDs. `nil` if 0 records at or after the cursor. + def next_range! + return unless @model_class.any? + + batch_first_id = cursor_id + + batch_last_id = get_batch_last_id(batch_first_id) + return unless batch_last_id + + batch_first_id..batch_last_id + end + + private + + # @private + # + # Get the last ID of the batch. Increment the cursor or reset it if at end. + # + # @param [Integer] batch_first_id the first ID of the batch + # @return [Integer] batch_last_id the last ID of the batch (not the table) + def get_batch_last_id(batch_first_id) + batch_last_id, more_rows = run_query(@model_class.table_name, @model_class.primary_key, batch_first_id, @batch_size) + + if more_rows + increment_batch(batch_last_id) + else + reset if batch_first_id > 1 + end + + batch_last_id + end + + def run_query(table, primary_key, batch_first_id, batch_size) + sql = <<~SQL + SELECT MAX(batch.id) AS batch_last_id, + EXISTS ( + SELECT #{primary_key} + FROM #{table} + WHERE #{primary_key} > MAX(batch.id) + ) AS more_rows + FROM ( + SELECT #{primary_key} + FROM #{table} + WHERE #{primary_key} >= #{batch_first_id} + ORDER BY #{primary_key} + LIMIT #{batch_size}) AS batch; + SQL + + result = ActiveRecord::Base.connection.exec_query(sql).first + + [result["batch_last_id"], result["more_rows"]] + end + + def reset + set_cursor_id(1) + end + + def increment_batch(batch_last_id) + set_cursor_id(batch_last_id + 1) + end + + # @private + # + # @return [Integer] the cursor ID, or 1 if it is not set + def cursor_id + Rails.cache.fetch("#{cache_key}:cursor_id") || 1 + end + + def set_cursor_id(id) + Rails.cache.write("#{cache_key}:cursor_id", id) + end + + def cache_key + @cache_key ||= "#{self.class.name.parameterize}:#{@model_class.name.parameterize}:#{@key}:cursor_id" + end + end +end diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb index f7699ef1718..bd69843adf1 100644 --- a/lib/gitlab/mail_room.rb +++ b/lib/gitlab/mail_room.rb @@ -2,6 +2,7 @@ require 'yaml' require 'json' +require 'pathname' require_relative 'redis/queues' unless defined?(Gitlab::Redis::Queues) # This service is run independently of the main Rails process, @@ -21,39 +22,60 @@ module Gitlab log_path: RAILS_ROOT_DIR.join('log', 'mail_room_json.log') }.freeze + # Email specific configuration which is merged with configuration + # fetched from YML config file. + ADDRESS_SPECIFIC_CONFIG = { + incoming_email: { + queue: 'email_receiver', + worker: 'EmailReceiverWorker' + }, + service_desk_email: { + queue: 'service_desk_email_receiver', + worker: 'ServiceDeskEmailReceiverWorker' + } + }.freeze + class << self - def enabled? - config[:enabled] && config[:address] + def enabled_configs + @enabled_configs ||= configs.select { |config| enabled?(config) } end - def config - @config ||= fetch_config - end + private - def reset_config! - @config = nil + def enabled?(config) + config[:enabled] && !config[:address].to_s.empty? end - private + def configs + ADDRESS_SPECIFIC_CONFIG.keys.map { |key| fetch_config(key) } + end - def fetch_config + def fetch_config(config_key) return {} unless File.exist?(config_file) - config = load_from_yaml || {} - config = DEFAULT_CONFIG.merge(config) do |_key, oldval, newval| + config = merged_configs(config_key) + config.merge!(redis_config) if enabled?(config) + config[:log_path] = File.expand_path(config[:log_path], RAILS_ROOT_DIR) + + config + end + + def merged_configs(config_key) + yml_config = load_yaml.fetch(config_key, {}) + specific_config = ADDRESS_SPECIFIC_CONFIG.fetch(config_key, {}) + DEFAULT_CONFIG.merge(specific_config, yml_config) do |_key, oldval, newval| newval.nil? ? oldval : newval end + end - if config[:enabled] && config[:address] - gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env) - config[:redis_url] = gitlab_redis_queues.url + def redis_config + gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env) + config = { redis_url: gitlab_redis_queues.url } - if gitlab_redis_queues.sentinels? - config[:sentinels] = gitlab_redis_queues.sentinels - end + if gitlab_redis_queues.sentinels? + config[:sentinels] = gitlab_redis_queues.sentinels end - config[:log_path] = File.expand_path(config[:log_path], RAILS_ROOT_DIR) config end @@ -65,8 +87,8 @@ module Gitlab ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../config/gitlab.yml', __dir__) end - def load_from_yaml - YAML.load_file(config_file)[rails_env].deep_symbolize_keys[:incoming_email] + def load_yaml + @yaml ||= YAML.load_file(config_file)[rails_env].deep_symbolize_keys end end end diff --git a/lib/gitlab/marginalia.rb b/lib/gitlab/marginalia.rb index 2be96cecae3..24e21a1d512 100644 --- a/lib/gitlab/marginalia.rb +++ b/lib/gitlab/marginalia.rb @@ -2,6 +2,8 @@ module Gitlab module Marginalia + cattr_accessor :enabled, default: false + MARGINALIA_FEATURE_FLAG = :marginalia def self.set_application_name @@ -15,14 +17,14 @@ module Gitlab end def self.cached_feature_enabled? - !!@enabled + enabled end def self.set_feature_cache # During db:create and db:bootstrap skip feature query as DB is not available yet. - return false unless ActiveRecord::Base.connected? && Gitlab::Database.cached_table_exists?('features') + return false unless Gitlab::Database.cached_table_exists?('features') - @enabled = Feature.enabled?(MARGINALIA_FEATURE_FLAG) + self.enabled = Feature.enabled?(MARGINALIA_FEATURE_FLAG) end end end diff --git a/lib/gitlab/metrics/dashboard/finder.rb b/lib/gitlab/metrics/dashboard/finder.rb index 268112f33a9..3dd86c8685d 100644 --- a/lib/gitlab/metrics/dashboard/finder.rb +++ b/lib/gitlab/metrics/dashboard/finder.rb @@ -34,6 +34,8 @@ module Gitlab # cluster, one of [:admin, :project, :group] # @param options - grafana_url [String] URL pointing # to a grafana dashboard panel + # @param options - prometheus_alert_id [Integer] ID of + # a PrometheusAlert. For dashboard embeds. # @return [Hash] def find(project, user, options = {}) service_for(options) @@ -63,7 +65,7 @@ module Gitlab def find_all_paths_from_source(project) Gitlab::Metrics::Dashboard::Cache.delete_all! - system_service.all_dashboard_paths(project) + default_dashboard_path(project) .+ project_service.all_dashboard_paths(project) end @@ -77,6 +79,18 @@ module Gitlab ::Metrics::Dashboard::ProjectDashboardService end + def self_monitoring_service + ::Metrics::Dashboard::SelfMonitoringDashboardService + end + + def default_dashboard_path(project) + if project.self_monitoring? + self_monitoring_service.all_dashboard_paths(project) + else + system_service.all_dashboard_paths(project) + end + end + def service_for(options) Gitlab::Metrics::Dashboard::ServiceSelector.call(options) end diff --git a/lib/gitlab/metrics/dashboard/service_selector.rb b/lib/gitlab/metrics/dashboard/service_selector.rb index 5b6f25420e0..24ea85a5a95 100644 --- a/lib/gitlab/metrics/dashboard/service_selector.rb +++ b/lib/gitlab/metrics/dashboard/service_selector.rb @@ -8,50 +8,40 @@ module Gitlab module Metrics module Dashboard class ServiceSelector - SERVICES = ::Metrics::Dashboard - class << self include Gitlab::Utils::StrongMemoize + SERVICES = [ + ::Metrics::Dashboard::CustomMetricEmbedService, + ::Metrics::Dashboard::GrafanaMetricEmbedService, + ::Metrics::Dashboard::DynamicEmbedService, + ::Metrics::Dashboard::DefaultEmbedService, + ::Metrics::Dashboard::SystemDashboardService, + ::Metrics::Dashboard::PodDashboardService, + ::Metrics::Dashboard::SelfMonitoringDashboardService, + ::Metrics::Dashboard::ProjectDashboardService + ].freeze + # Returns a class which inherits from the BaseService - # class that can be used to obtain a dashboard. + # class that can be used to obtain a dashboard for + # the provided params. # @return [Gitlab::Metrics::Dashboard::Services::BaseService] def call(params) - return SERVICES::CustomMetricEmbedService if custom_metric_embed?(params) - return SERVICES::GrafanaMetricEmbedService if grafana_metric_embed?(params) - return SERVICES::DynamicEmbedService if dynamic_embed?(params) - return SERVICES::DefaultEmbedService if params[:embedded] - return SERVICES::SystemDashboardService if system_dashboard?(params[:dashboard_path]) - return SERVICES::PodDashboardService if pod_dashboard?(params[:dashboard_path]) - return SERVICES::ProjectDashboardService if params[:dashboard_path] - - default_service - end + service = services.find do |service_class| + service_class.valid_params?(params) + end - private - - def default_service - SERVICES::SystemDashboardService + service || default_service end - def system_dashboard?(filepath) - SERVICES::SystemDashboardService.matching_dashboard?(filepath) - end - - def pod_dashboard?(filepath) - SERVICES::PodDashboardService.matching_dashboard?(filepath) - end - - def custom_metric_embed?(params) - SERVICES::CustomMetricEmbedService.valid_params?(params) - end + private - def grafana_metric_embed?(params) - SERVICES::GrafanaMetricEmbedService.valid_params?(params) + def services + SERVICES end - def dynamic_embed?(params) - SERVICES::DynamicEmbedService.valid_params?(params) + def default_service + ::Metrics::Dashboard::SystemDashboardService end end end diff --git a/lib/gitlab/metrics/dashboard/stages/project_metrics_details_inserter.rb b/lib/gitlab/metrics/dashboard/stages/project_metrics_details_inserter.rb new file mode 100644 index 00000000000..a9d11f58255 --- /dev/null +++ b/lib/gitlab/metrics/dashboard/stages/project_metrics_details_inserter.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module Metrics + module Dashboard + module Stages + class ProjectMetricsDetailsInserter < BaseStage + def transform! + dashboard[:panel_groups].each do |panel_group| + next unless panel_group + + has_custom_metrics = custom_group_titles.include?(panel_group[:group]) + panel_group[:has_custom_metrics] = has_custom_metrics + + panel_group[:panels].each do |panel| + next unless panel + + panel[:metrics].each do |metric| + next unless metric + + metric[:edit_path] = has_custom_metrics ? edit_path(metric) : nil + end + end + end + end + + private + + def custom_group_titles + @custom_group_titles ||= PrometheusMetricEnums.custom_group_details.values.map { |group_details| group_details[:group_title] } + end + + def edit_path(metric) + Gitlab::Routing.url_helpers.edit_project_prometheus_metric_path(project, metric[:metric_id]) + end + end + end + end + end +end diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb index 712f769bbeb..1d948883151 100644 --- a/lib/gitlab/metrics/dashboard/url.rb +++ b/lib/gitlab/metrics/dashboard/url.rb @@ -6,41 +6,41 @@ module Gitlab module Dashboard class Url class << self + include Gitlab::Utils::StrongMemoize + + QUERY_PATTERN = '(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?' + ANCHOR_PATTERN = '(?<anchor>\#[a-z0-9_-]+)?' + OPTIONAL_DASH_PATTERN = '(?:/-)?' + # Matches urls for a metrics dashboard. This could be # either the /metrics endpoint or the /metrics_dashboard # endpoint. # # EX - https://<host>/<namespace>/<project>/environments/<env_id>/metrics - def regex - %r{ - (?<url> - #{gitlab_pattern} - #{project_pattern} - (?:\/\-)? - \/environments - \/(?<environment>\d+) - \/metrics - #{query_pattern} - #{anchor_pattern} + def metrics_regex + strong_memoize(:metrics_regex) do + regex_for_project_metrics( + %r{ + /environments + /(?<environment>\d+) + /metrics + }x ) - }x + end end # Matches dashboard urls for a Grafana embed. # # EX - https://<host>/<namespace>/<project>/grafana/metrics_dashboard def grafana_regex - %r{ - (?<url> - #{gitlab_pattern} - #{project_pattern} - (?:\/\-)? - \/grafana - \/metrics_dashboard - #{query_pattern} - #{anchor_pattern} + strong_memoize(:grafana_regex) do + regex_for_project_metrics( + %r{ + /grafana + /metrics_dashboard + }x ) - }x + end end # Parses query params out from full url string into hash. @@ -62,23 +62,30 @@ module Gitlab private - def gitlab_pattern - Regexp.escape(Gitlab.config.gitlab.url) - end - - def project_pattern - "\/#{Project.reference_pattern}" + def regex_for_project_metrics(path_suffix_pattern) + %r{ + (?<url> + #{gitlab_host_pattern} + #{project_path_pattern} + #{OPTIONAL_DASH_PATTERN} + #{path_suffix_pattern} + #{QUERY_PATTERN} + #{ANCHOR_PATTERN} + ) + }x end - def query_pattern - '(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?' + def gitlab_host_pattern + Regexp.escape(Gitlab.config.gitlab.url) end - def anchor_pattern - '(?<anchor>\#[a-z0-9_-]+)?' + def project_path_pattern + "\/#{Project.reference_pattern}" end end end end end end + +Gitlab::Metrics::Dashboard::Url.extend_if_ee('::EE::Gitlab::Metrics::Dashboard::Url') diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index f207d91235f..53508938c49 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -60,7 +60,7 @@ module Gitlab end meta_import_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{repository_url}" - meta_source_tag = tag :meta, name: 'go-source', content: "#{import_prefix} #{project_url} #{project_url}/tree/#{branch}{/dir} #{project_url}/blob/#{branch}{/dir}/{file}#L{line}" + meta_source_tag = tag :meta, name: 'go-source', content: "#{import_prefix} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}" head_tag = content_tag :head, meta_import_tag + meta_source_tag html_tag = content_tag :html, head_tag + body_tag [html_tag, 200] diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb index c749816cf6a..ca8f4e34802 100644 --- a/lib/gitlab/middleware/read_only/controller.rb +++ b/lib/gitlab/middleware/read_only/controller.rb @@ -12,20 +12,21 @@ module Gitlab ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance' WHITELISTED_GIT_ROUTES = { - 'projects/git_http' => %w{git_upload_pack git_receive_pack} + 'repositories/git_http' => %w{git_upload_pack git_receive_pack} }.freeze WHITELISTED_GIT_LFS_ROUTES = { - 'projects/lfs_api' => %w{batch}, - 'projects/lfs_locks_api' => %w{verify create unlock} + 'repositories/lfs_api' => %w{batch}, + 'repositories/lfs_locks_api' => %w{verify create unlock} }.freeze WHITELISTED_GIT_REVISION_ROUTES = { 'projects/compare' => %w{create} }.freeze - WHITELISTED_LOGOUT_ROUTES = { - 'sessions' => %w{destroy} + WHITELISTED_SESSION_ROUTES = { + 'sessions' => %w{destroy}, + 'admin/sessions' => %w{create destroy} }.freeze GRAPHQL_URL = '/api/graphql' @@ -89,7 +90,7 @@ module Gitlab # Overridden in EE module def whitelisted_routes - grack_route? || internal_route? || lfs_route? || compare_git_revisions_route? || sidekiq_route? || logout_route? || graphql_query? + grack_route? || internal_route? || lfs_route? || compare_git_revisions_route? || sidekiq_route? || session_route? || graphql_query? end def grack_route? @@ -122,11 +123,12 @@ module Gitlab WHITELISTED_GIT_LFS_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) end - def logout_route? + def session_route? # Calling route_hash may be expensive. Only do it if we think there's a possible match - return false unless request.post? && request.path.end_with?('/users/sign_out') + return false unless request.post? && request.path.end_with?('/users/sign_out', + '/admin/session', '/admin/session/destroy') - WHITELISTED_LOGOUT_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) + WHITELISTED_SESSION_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) end def sidekiq_route? diff --git a/lib/gitlab/patch/active_record_query_cache.rb b/lib/gitlab/patch/active_record_query_cache.rb deleted file mode 100644 index d6b649cdea7..00000000000 --- a/lib/gitlab/patch/active_record_query_cache.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -# Fixes a bug where the query cache isn't aware of the shared -# ActiveRecord connection used in tests -# https://github.com/rails/rails/issues/36587 - -# To be removed with https://gitlab.com/gitlab-org/gitlab-foss/issues/64413 - -module Gitlab - module Patch - module ActiveRecordQueryCache - # rubocop:disable Gitlab/ModuleWithInstanceVariables - def enable_query_cache! - @query_cache_enabled[connection_cache_key(current_thread)] = true - connection.enable_query_cache! if active_connection? - end - - def disable_query_cache! - @query_cache_enabled.delete connection_cache_key(current_thread) - connection.disable_query_cache! if active_connection? - end - - def query_cache_enabled - @query_cache_enabled[connection_cache_key(current_thread)] - end - - def active_connection? - @thread_cached_conns[connection_cache_key(current_thread)] - end - - private - - def current_thread - @lock_thread || Thread.current - end - # rubocop:enable Gitlab/ModuleWithInstanceVariables - end - end -end diff --git a/lib/gitlab/phabricator_import/base_worker.rb b/lib/gitlab/phabricator_import/base_worker.rb deleted file mode 100644 index d2c2ef8db48..00000000000 --- a/lib/gitlab/phabricator_import/base_worker.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -# All workers within a Phabricator import should inherit from this worker and -# implement the `#import` method. The jobs should then be scheduled using the -# `.schedule` class method instead of `.perform_async` -# -# Doing this makes sure that only one job of that type is running at the same time -# for a certain project. This will avoid deadlocks. When a job is already running -# we'll wait for it for 10 times 5 seconds to restart. If the running job hasn't -# finished, by then, we'll retry in 30 seconds. -# -# It also makes sure that we keep the import state of the project up to date: -# - It keeps track of the jobs so we know how many jobs are running for the -# project -# - It refreshes the import jid, so it doesn't get cleaned up by the -# `StuckImportJobsWorker` -# - It marks the import as failed if a job failed to many times -# - It marks the import as finished when all remaining jobs are done -module Gitlab - module PhabricatorImport - class BaseWorker - include ApplicationWorker - include ProjectImportOptions # This marks the project as failed after too many tries - include Gitlab::ExclusiveLeaseHelpers - - feature_category :importers - - class << self - def schedule(project_id, *args) - perform_async(project_id, *args) - add_job(project_id) - end - - def add_job(project_id) - worker_state(project_id).add_job - end - - def remove_job(project_id) - worker_state(project_id).remove_job - end - - def worker_state(project_id) - Gitlab::PhabricatorImport::WorkerState.new(project_id) - end - end - - def perform(project_id, *args) - in_lock("#{self.class.name.underscore}/#{project_id}/#{args}", ttl: 2.hours, sleep_sec: 5.seconds) do - project = Project.find_by_id(project_id) - next unless project - - # Bail if the import job already failed - next unless project.import_state&.in_progress? - - project.import_state.refresh_jid_expiration - - import(project, *args) - - # If this is the last running job, finish the import - project.after_import if self.class.worker_state(project_id).running_count < 2 - - self.class.remove_job(project_id) - end - rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError - # Reschedule a job if there was already a running one - # Running them at the same time could cause a deadlock updating the same - # resource - self.class.perform_in(30.seconds, project_id, *args) - end - - private - - def import(project, *args) - importer_class.new(project, *args).execute - end - - def importer_class - raise NotImplementedError, "Implement `#{__method__}` on #{self.class}" - end - end - end -end diff --git a/lib/gitlab/phabricator_import/import_tasks_worker.rb b/lib/gitlab/phabricator_import/import_tasks_worker.rb deleted file mode 100644 index c36954a8d41..00000000000 --- a/lib/gitlab/phabricator_import/import_tasks_worker.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true -module Gitlab - module PhabricatorImport - class ImportTasksWorker < BaseWorker - def importer_class - Gitlab::PhabricatorImport::Issues::Importer - end - end - end -end diff --git a/lib/gitlab/profiler/total_time_flat_printer.rb b/lib/gitlab/profiler/total_time_flat_printer.rb index 2c105d2722b..9846bad3c08 100644 --- a/lib/gitlab/profiler/total_time_flat_printer.rb +++ b/lib/gitlab/profiler/total_time_flat_printer.rb @@ -24,7 +24,7 @@ module Gitlab sum += method.self_time - @output << "%6.2f %9.3f %9.3f %9.3f %9.3f %8d %s%s\n" % [ + @output << "%6.2f %9.3f %9.3f %9.3f %9.3f %8d %s%-30s %s\n" % [ method.self_time / total_time * 100, # %self method.total_time, # total method.self_time, # self @@ -32,7 +32,8 @@ module Gitlab method.children_time, # children method.called, # calls method.recursive? ? "*" : " ", # cycle - method_name(method) # name + method.full_name, # method_name + method_location(method) # location ] end end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 2669adb8455..eb7ca80dd60 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -2,7 +2,7 @@ module Gitlab class ProjectSearchResults < SearchResults - attr_reader :project, :repository_ref + attr_reader :project, :repository_ref, :per_page def initialize(current_user, project, query, repository_ref = nil, per_page: 20) @current_user = current_user @@ -17,7 +17,7 @@ module Gitlab when 'notes' notes.page(page).per(per_page) when 'blobs' - paginated_blobs(blobs, page) + paginated_blobs(blobs(page), page) when 'wiki_blobs' paginated_blobs(wiki_blobs, page) when 'commits' @@ -32,7 +32,7 @@ module Gitlab def formatted_count(scope) case scope when 'blobs' - blobs_count.to_s + formatted_limited_count(limited_blobs_count) when 'notes' formatted_limited_count(limited_notes_count) when 'wiki_blobs' @@ -48,8 +48,8 @@ module Gitlab super.where(id: @project.team.members) # rubocop:disable CodeReuse/ActiveRecord end - def blobs_count - @blobs_count ||= blobs.count + def limited_blobs_count + @limited_blobs_count ||= blobs.count end # rubocop: disable CodeReuse/ActiveRecord @@ -81,7 +81,7 @@ module Gitlab counts = %i(limited_milestones_count limited_notes_count limited_merge_requests_count limited_issues_count - blobs_count wiki_blobs_count) + limited_blobs_count wiki_blobs_count) counts.all? { |count_method| public_send(count_method).zero? } # rubocop:disable GitlabSecurity/PublicSend end @@ -95,10 +95,16 @@ module Gitlab results end - def blobs + def limit_up_to_page(page) + current_page = page&.to_i || 1 + offset = per_page * (current_page - 1) + count_limit + offset + end + + def blobs(page = 1) return [] unless Ability.allowed?(@current_user, :download_code, @project) - @blobs ||= Gitlab::FileFinder.new(project, repository_project_ref).find(query) + @blobs ||= Gitlab::FileFinder.new(project, repository_project_ref).find(query, content_match_cutoff: limit_up_to_page(page)) end def wiki_blobs @@ -106,10 +112,10 @@ module Gitlab @wiki_blobs ||= begin if project.wiki_enabled? && query.present? - unless project.wiki.empty? - Gitlab::WikiFileFinder.new(project, repository_wiki_ref).find(query) - else + if project.wiki.empty? [] + else + Gitlab::WikiFileFinder.new(project, repository_wiki_ref).find(query) end else [] diff --git a/lib/gitlab/project_transfer.rb b/lib/gitlab/project_transfer.rb index d8f1d1e2316..f5a8aeff73c 100644 --- a/lib/gitlab/project_transfer.rb +++ b/lib/gitlab/project_transfer.rb @@ -32,7 +32,7 @@ module Gitlab private def move(path_was, path, base_dir = nil) - base_dir = root_dir unless base_dir + base_dir ||= root_dir from = File.join(base_dir, path_was) to = File.join(base_dir, path) FileUtils.mv(from, to) diff --git a/lib/gitlab/prometheus/queries/knative_invocation_query.rb b/lib/gitlab/prometheus/queries/knative_invocation_query.rb index 8873608c411..abc90bad9c3 100644 --- a/lib/gitlab/prometheus/queries/knative_invocation_query.rb +++ b/lib/gitlab/prometheus/queries/knative_invocation_query.rb @@ -20,7 +20,7 @@ module Gitlab protected def context(function_id) - function = Serverless::Function.find_by_id(function_id) + function = ::Serverless::Function.find_by_id(function_id) { function_name: function.name, kube_namespace: function.namespace diff --git a/lib/gitlab/query_limiting/middleware.rb b/lib/gitlab/query_limiting/middleware.rb index f6ffbfe2645..714fe42884a 100644 --- a/lib/gitlab/query_limiting/middleware.rb +++ b/lib/gitlab/query_limiting/middleware.rb @@ -37,10 +37,10 @@ module Gitlab controller = env[CONTROLLER_KEY] action = "#{controller.class.name}##{controller.action_name}" - if controller.content_type == 'text/html' + if controller.media_type == 'text/html' action else - "#{action} (#{controller.content_type})" + "#{action} (#{controller.media_type})" end end diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb index e04d6f250b1..6f87968e286 100644 --- a/lib/gitlab/quick_actions/extractor.rb +++ b/lib/gitlab/quick_actions/extractor.rb @@ -34,26 +34,55 @@ module Gitlab def extract_commands(content, only: nil) return [content, []] unless content - content = content.dup + content, commands = perform_regex(content, only: only) - commands = [] + perform_substitutions(content, commands) + end + # Encloses quick action commands into code span markdown + # avoiding them being executed, for example, when sent via email + # to GitLab service desk. + # Example: /label ~label1 becomes `/label ~label1` + def redact_commands(content) + return "" unless content + + content, _ = perform_regex(content, redact: true) + + content + end + + private + + def perform_regex(content, only: nil, redact: false) + commands = [] + content = content.dup content.delete!("\r") + content.gsub!(commands_regex(only: only)) do - if $~[:cmd] - commands << [$~[:cmd].downcase, $~[:arg]].reject(&:blank?) - '' - else - $~[0] - end + command, output = process_commands($~, redact) + commands << command + output end - content, commands = perform_substitutions(content, commands) - - [content.rstrip, commands] + [content.rstrip, commands.reject(&:empty?)] end - private + def process_commands(matched_text, redact) + output = matched_text[0] + command = [] + + if matched_text[:cmd] + command = [matched_text[:cmd].downcase, matched_text[:arg]].reject(&:blank?) + output = '' + + if redact + output = "`/#{matched_text[:cmd]}#{" " + matched_text[:arg] if matched_text[:arg]}`" + output += "\n" if matched_text[0].include?("\n") + end + end + + [command, output] + end # Builds a regular expression to match known commands. # First match group captures the command name and 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 183191f31a6..aff3ed53734 100644 --- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb +++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb @@ -12,6 +12,15 @@ module Gitlab explanation do |users| _('Assigns %{assignee_users_sentence}.') % { assignee_users_sentence: assignee_users_sentence(users) } end + execution_message do |users = nil| + if users.blank? + _("Failed to assign a user because no user was found.") + else + users = [users.first] unless quick_action_target.allows_multiple_assignees? + + _('Assigned %{assignee_users_sentence}.') % { assignee_users_sentence: assignee_users_sentence(users) } + end + end params do quick_action_target.allows_multiple_assignees? ? '@user1 @user2' : '@user' end @@ -23,19 +32,14 @@ module Gitlab extract_users(assignee_param) end command :assign do |users| - if users.empty? - @execution_message[:assign] = _("Failed to assign a user because no user was found.") - next - end + next if users.empty? if quick_action_target.allows_multiple_assignees? @updates[:assignee_ids] ||= quick_action_target.assignees.map(&:id) - @updates[:assignee_ids] += users.map(&:id) + @updates[:assignee_ids] |= users.map(&:id) else @updates[:assignee_ids] = [users.first.id] end - - @execution_message[:assign] = _('Assigned %{assignee_users_sentence}.') % { assignee_users_sentence: assignee_users_sentence(users) } end desc do @@ -249,7 +253,7 @@ module Gitlab def assignees_for_removal(users) assignees = quick_action_target.assignees if users.present? && quick_action_target.allows_multiple_assignees? - assignees & users + users else assignees end diff --git a/lib/gitlab/quick_actions/substitution_definition.rb b/lib/gitlab/quick_actions/substitution_definition.rb index 0fda056a4fe..b7231aa3a8b 100644 --- a/lib/gitlab/quick_actions/substitution_definition.rb +++ b/lib/gitlab/quick_actions/substitution_definition.rb @@ -10,14 +10,14 @@ module Gitlab end def match(content) - content.match %r{^/#{all_names.join('|')} ?(.*)$} + content.match %r{^/#{all_names.join('|')}(?![\S]) ?(.*)$} end def perform_substitution(context, content) return unless content all_names.each do |a_name| - content = content.gsub(%r{/#{a_name} ?(.*)$}i, execute_block(action_block, context, '\1')) + content = content.gsub(%r{/#{a_name}(?![\S]) ?(.*)$}i, execute_block(action_block, context, '\1')) end content diff --git a/lib/gitlab/redis/boolean.rb b/lib/gitlab/redis/boolean.rb new file mode 100644 index 00000000000..9b0b20fc2be --- /dev/null +++ b/lib/gitlab/redis/boolean.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +# A serializer for boolean values being stored in Redis. +# +# This is to ensure that booleans are stored in a consistent and +# testable way when being stored as strings in Redis. +# +# Examples: +# +# bool = Gitlab::Redis::Boolean.new(true) +# bool.to_s == "_b:1" +# +# Gitlab::Redis::Boolean.encode(true) +# => "_b:1" +# +# Gitlab::Redis::Boolean.decode("_b:1") +# => true +# +# Gitlab::Redis::Boolean.true?("_b:1") +# => true +# +# Gitlab::Redis::Boolean.true?("_b:0") +# => false + +module Gitlab + module Redis + class Boolean + LABEL = "_b" + DELIMITER = ":" + TRUE_STR = "1" + FALSE_STR = "0" + + BooleanError = Class.new(StandardError) + NotABooleanError = Class.new(BooleanError) + NotAnEncodedBooleanStringError = Class.new(BooleanError) + + def initialize(value) + @value = value + end + + # @return [String] the encoded boolean + def to_s + self.class.encode(@value) + end + + class << self + # Turn a boolean into a string for storage in Redis + # + # @param value [Boolean] true or false + # @return [String] the encoded boolean + # @raise [NotABooleanError] if the value isn't true or false + def encode(value) + raise NotABooleanError.new(value) unless bool?(value) + + [LABEL, to_string(value)].join(DELIMITER) + end + + # Decode a boolean string + # + # @param value [String] the stored boolean string + # @return [Boolean] true or false + # @raise [NotAnEncodedBooleanStringError] if the provided value isn't an encoded boolean + def decode(value) + raise NotAnEncodedBooleanStringError.new(value.class) unless value.is_a?(String) + + label, bool_str = *value.split(DELIMITER, 2) + + raise NotAnEncodedBooleanStringError.new(label) unless label == LABEL + + from_string(bool_str) + end + + # Decode a boolean string, then test if it's true + # + # @param value [String] the stored boolean string + # @return [Boolean] is the value true? + # @raise [NotAnEncodedBooleanStringError] if the provided value isn't an encoded boolean + def true?(encoded_value) + decode(encoded_value) + end + + # Decode a boolean string, then test if it's false + # + # @param value [String] the stored boolean string + # @return [Boolean] is the value false? + # @raise [NotAnEncodedBooleanStringError] if the provided value isn't an encoded boolean + def false?(encoded_value) + !true?(encoded_value) + end + + private + + def bool?(value) + [true, false].include?(value) + end + + def to_string(bool) + bool ? TRUE_STR : FALSE_STR + end + + def from_string(str) + raise NotAnEncodedBooleanStringError.new(str) unless [TRUE_STR, FALSE_STR].include?(str) + + str == TRUE_STR + end + end + end + end +end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 48eaf073e8a..fd6e24a96d8 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -5,6 +5,9 @@ module Gitlab extend self def project_name_regex + # The character range \p{Alnum} overlaps with \u{00A9}-\u{1f9ff} + # hence the Ruby warning. + # https://gitlab.com/gitlab-org/gitlab/merge_requests/23165#not-easy-fixable @project_name_regex ||= /\A[\p{Alnum}\u{00A9}-\u{1f9ff}_][\p{Alnum}\p{Pd}\u{00A9}-\u{1f9ff}_\. ]*\z/.freeze end diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb index 1baa2a9e461..e8c749cac14 100644 --- a/lib/gitlab/repo_path.rb +++ b/lib/gitlab/repo_path.rb @@ -4,8 +4,9 @@ module Gitlab module RepoPath NotFoundError = Class.new(StandardError) - def self.parse(repo_path) - project_path = repo_path.sub(/\.git\z/, '').sub(%r{\A/}, '') + def self.parse(path) + repo_path = path.sub(/\.git\z/, '').sub(%r{\A/}, '') + redirected_path = nil # Detect the repo type based on the path, the first one tried is the project # type, which does not have a suffix. @@ -14,10 +15,13 @@ module Gitlab # type. # We'll always try to find a project with an empty suffix (for the # `Gitlab::GlRepository::PROJECT` type. - next unless project_path.end_with?(type.path_suffix) + next unless type.valid?(repo_path) - project, was_redirected = find_project(project_path.chomp(type.path_suffix)) - redirected_path = project_path if was_redirected + # Removing the suffix (.wiki, .design, ...) from the project path + full_path = repo_path.chomp(type.path_suffix) + + project, was_redirected = find_project(full_path) + redirected_path = repo_path if was_redirected # If we found a matching project, then the type was matched, no need to # continue looking. diff --git a/lib/gitlab/repository_cache.rb b/lib/gitlab/repository_cache.rb index fca8c43da2e..dc8b2467f72 100644 --- a/lib/gitlab/repository_cache.rb +++ b/lib/gitlab/repository_cache.rb @@ -33,8 +33,8 @@ module Gitlab backend.read(cache_key(key)) end - def write(key, value) - backend.write(cache_key(key), value) + def write(key, value, *args) + backend.write(cache_key(key), value, *args) end def fetch_without_caching_false(key, &block) diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb index b2dc92ce010..304f53b58c4 100644 --- a/lib/gitlab/repository_cache_adapter.rb +++ b/lib/gitlab/repository_cache_adapter.rb @@ -132,6 +132,11 @@ module Gitlab raise NotImplementedError end + # RepositoryHashCache to be used. Should be overridden by the including class + def redis_hash_cache + raise NotImplementedError + end + # List of cached methods. Should be overridden by the including class def cached_methods raise NotImplementedError @@ -215,6 +220,7 @@ module Gitlab end expire_redis_set_method_caches(methods) + expire_redis_hash_method_caches(methods) expire_request_store_method_caches(methods) end @@ -234,6 +240,10 @@ module Gitlab methods.each { |name| redis_set_cache.expire(name) } end + def expire_redis_hash_method_caches(methods) + methods.each { |name| redis_hash_cache.delete(name) } + end + # All cached repository methods depend on the existence of a Git repository, # so if the repository doesn't exist, we already know not to call it. def fallback_early?(method_name) diff --git a/lib/gitlab/repository_hash_cache.rb b/lib/gitlab/repository_hash_cache.rb new file mode 100644 index 00000000000..d2a7b450000 --- /dev/null +++ b/lib/gitlab/repository_hash_cache.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +# Interface to the Redis-backed cache store for keys that use a Redis HSET. +# This is currently used as an incremental cache by the `Repository` model +# for `#merged_branch_names`. It works slightly differently to the other +# repository cache classes in that it is intended to work with partial +# caches which can be updated with new data, using the Redis hash system. + +module Gitlab + class RepositoryHashCache + attr_reader :repository, :namespace, :expires_in + + RepositoryHashCacheError = Class.new(StandardError) + InvalidKeysProvidedError = Class.new(RepositoryHashCacheError) + InvalidHashProvidedError = Class.new(RepositoryHashCacheError) + + # @param repository [Repository] + # @param extra_namespace [String] + # @param expires_in [Integer] expiry time for hash store keys + def initialize(repository, extra_namespace: nil, expires_in: 1.day) + @repository = repository + @namespace = "#{repository.full_path}" + @namespace += ":#{repository.project.id}" if repository.project + @namespace = "#{@namespace}:#{extra_namespace}" if extra_namespace + @expires_in = expires_in + end + + # @param type [String] + # @return [String] + def cache_key(type) + "#{type}:#{namespace}:hash" + end + + # @param key [String] + # @return [Integer] 0 or 1 depending on success + def delete(key) + with { |redis| redis.del(cache_key(key)) } + end + + # Check if the provided hash key exists in the hash. + # + # @param key [String] + # @param h_key [String] the key to check presence in Redis + # @return [True, False] + def key?(key, h_key) + with { |redis| redis.hexists(cache_key(key), h_key) } + end + + # Read the values of a set of keys from the hash store, and return them + # as a hash of those keys and their values. + # + # @param key [String] + # @param hash_keys [Array<String>] an array of keys to retrieve from the store + # @return [Hash] a Ruby hash of the provided keys and their values from the store + def read_members(key, hash_keys = []) + raise InvalidKeysProvidedError unless hash_keys.is_a?(Array) && hash_keys.any? + + with do |redis| + # Fetch an array of values for the supplied keys + values = redis.hmget(cache_key(key), hash_keys) + + # Turn it back into a hash + hash_keys.zip(values).to_h + end + end + + # Write a hash to the store. All keys and values will be strings when stored. + # + # @param key [String] + # @param hash [Hash] the hash to be written to Redis + # @return [Boolean] whether all operations were successful or not + def write(key, hash) + raise InvalidHashProvidedError unless hash.is_a?(Hash) && hash.any? + + full_key = cache_key(key) + + with do |redis| + results = redis.pipelined do + # Set each hash key to the provided value + hash.each do |h_key, h_value| + redis.hset(full_key, h_key, h_value) + end + + # Update the expiry time for this hset + redis.expire(full_key, expires_in) + end + + results.all? + end + end + + # A variation on the `fetch` pattern of other cache stores. This method + # allows you to attempt to fetch a group of keys from the hash store, and + # for any keys that are missing values a block is then called to provide + # those values, which are then written back into Redis. Both sets of data + # are then combined and returned as one hash. + # + # @param key [String] + # @param h_keys [Array<String>] the keys to fetch or add to the cache + # @yieldparam missing_keys [Array<String>] the keys missing from the cache + # @yieldparam new_values [Hash] the hash to be populated by the block + # @return [Hash] the amalgamated hash of cached and uncached values + def fetch_and_add_missing(key, h_keys, &block) + # Check the cache for all supplied keys + cache_values = read_members(key, h_keys) + + # Find the results which returned nil (meaning they're not in the cache) + missing = cache_values.select { |_, v| v.nil? }.keys + + if missing.any? + new_values = {} + + # Run the block, which updates the new_values hash + yield(missing, new_values) + + # Ensure all values are converted to strings, to ensure merging hashes + # below returns standardised data. + new_values = standardize_hash(new_values) + + # Write the new values to the hset + write(key, new_values) + + # Merge the two sets of values to return a complete hash + cache_values.merge!(new_values) + end + + record_metrics(key, cache_values, missing) + + cache_values + end + + private + + def with(&blk) + Gitlab::Redis::Cache.with(&blk) # rubocop:disable CodeReuse/ActiveRecord + end + + # Take a hash and convert both keys and values to strings, for insertion into Redis. + # + # @param hash [Hash] + # @return [Hash] the stringified hash + def standardize_hash(hash) + hash.map { |k, v| [k.to_s, v.to_s] }.to_h + end + + # Record metrics in Prometheus. + # + # @param key [String] the basic key, e.g. :merged_branch_names. Not record-specific. + # @param cache_values [Hash] the hash response from the cache read + # @param missing_keys [Array<String>] the array of missing keys from the cache read + def record_metrics(key, cache_values, missing_keys) + cache_hits = cache_values.delete_if { |_, v| v.nil? } + + # Increment the counter if we have hits + metrics_hit_counter.increment(full_hit: missing_keys.empty?, store_type: key) if cache_hits.any? + + # Track the number of hits we got + metrics_hit_histogram.observe({ type: "hits", store_type: key }, cache_hits.size) + metrics_hit_histogram.observe({ type: "misses", store_type: key }, missing_keys.size) + end + + def metrics_hit_counter + @counter ||= Gitlab::Metrics.counter( + :gitlab_repository_hash_cache_hit, + "Count of cache hits in Redis HSET" + ) + end + + def metrics_hit_histogram + @histogram ||= Gitlab::Metrics.histogram( + :gitlab_repository_hash_cache_size, + "Number of records in the HSET" + ) + end + end +end diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb index 214670cac28..9da6732796a 100644 --- a/lib/gitlab/request_context.rb +++ b/lib/gitlab/request_context.rb @@ -18,7 +18,6 @@ module Gitlab def request_deadline strong_memoize(:request_deadline) do next unless request_start_time - next unless Feature.enabled?(:request_deadline) request_start_time + max_request_duration_seconds end diff --git a/lib/gitlab/runtime.rb b/lib/gitlab/runtime.rb index 97f7a8e2800..bf579dd3b77 100644 --- a/lib/gitlab/runtime.rb +++ b/lib/gitlab/runtime.rb @@ -8,15 +8,20 @@ module Gitlab AmbiguousProcessError = Class.new(IdentificationError) UnknownProcessError = Class.new(IdentificationError) + AVAILABLE_RUNTIMES = [ + :console, + :geo_log_cursor, + :puma, + :rails_runner, + :rake, + :sidekiq, + :test_suite, + :unicorn + ].freeze + class << self def identify - matches = [] - matches << :puma if puma? - matches << :unicorn if unicorn? - matches << :console if console? - matches << :sidekiq if sidekiq? - matches << :rake if rake? - matches << :rspec if rspec? + matches = AVAILABLE_RUNTIMES.select { |runtime| public_send("#{runtime}?") } # rubocop:disable GitlabSecurity/PublicSend if matches.one? matches.first @@ -48,14 +53,22 @@ module Gitlab !!(defined?(::Rake) && Rake.application.top_level_tasks.any?) end - def rspec? - Rails.env.test? && process_name == 'rspec' + def test_suite? + Rails.env.test? end def console? !!defined?(::Rails::Console) end + def geo_log_cursor? + !!defined?(::GeoLogCursorOptionParser) + end + + def rails_runner? + !!defined?(::Rails::Command::RunnerCommand) + end + def web_server? puma? || unicorn? end @@ -64,17 +77,17 @@ module Gitlab puma? || sidekiq? end - def process_name - File.basename($0) - end - def max_threads + main_thread = 1 + if puma? - Puma.cli_config.options[:max_threads] + Puma.cli_config.options[:max_threads] + main_thread elsif sidekiq? - Sidekiq.options[:concurrency] + # An extra thread for the poller in Sidekiq Cron: + # https://github.com/ondrejbartas/sidekiq-cron#under-the-hood + Sidekiq.options[:concurrency] + main_thread + 1 else - 1 + main_thread end end end diff --git a/lib/gitlab/search/found_blob.rb b/lib/gitlab/search/found_blob.rb index 360239a84e4..f472c70446c 100644 --- a/lib/gitlab/search/found_blob.rb +++ b/lib/gitlab/search/found_blob.rb @@ -7,6 +7,7 @@ module Gitlab include Presentable include BlobLanguageFromGitAttributes include Gitlab::Utils::StrongMemoize + include BlobActiveModel attr_reader :project, :content_match, :blob_path diff --git a/lib/gitlab/search/found_wiki_page.rb b/lib/gitlab/search/found_wiki_page.rb new file mode 100644 index 00000000000..99ca6a79fe2 --- /dev/null +++ b/lib/gitlab/search/found_wiki_page.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# - rendering by using data purely from Elasticsearch and does not trigger Gitaly calls. +# - allows policy check +module Gitlab + module Search + class FoundWikiPage < SimpleDelegator + attr_reader :wiki + + def self.declarative_policy_class + 'WikiPagePolicy' + end + + # @param found_blob [Gitlab::Search::FoundBlob] + def initialize(found_blob) + super + @wiki = found_blob.project.wiki + end + + def to_ability_name + 'wiki_page' + end + end + end +end diff --git a/lib/gitlab/serverless/domain.rb b/lib/gitlab/serverless/domain.rb new file mode 100644 index 00000000000..ec7c68764d1 --- /dev/null +++ b/lib/gitlab/serverless/domain.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Gitlab + module Serverless + class Domain + UUID_LENGTH = 14 + + def self.generate_uuid + SecureRandom.hex(UUID_LENGTH / 2) + end + end + end +end diff --git a/lib/gitlab/serverless/function_uri.rb b/lib/gitlab/serverless/function_uri.rb new file mode 100644 index 00000000000..c0e0cf00f35 --- /dev/null +++ b/lib/gitlab/serverless/function_uri.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Serverless + class FunctionURI < URI::HTTPS + SERVERLESS_DOMAIN_REGEXP = %r{^(?<scheme>https?://)?(?<function>[^.]+)-(?<cluster_left>\h{2})a1(?<cluster_middle>\h{10})f2(?<cluster_right>\h{2})(?<environment_id>\h+)-(?<environment_slug>[^.]+)\.(?<domain>.+)}.freeze + + attr_reader :function, :cluster, :environment + + def initialize(function: nil, cluster: nil, environment: nil) + initialize_required_argument(:function, function) + initialize_required_argument(:cluster, cluster) + initialize_required_argument(:environment, environment) + + @host = "#{function}-#{cluster.uuid[0..1]}a1#{cluster.uuid[2..-3]}f2#{cluster.uuid[-2..-1]}#{"%x" % environment.id}-#{environment.slug}.#{cluster.domain}" + + super('https', nil, host, nil, nil, nil, nil, nil, nil) + end + + def self.parse(uri) + match = SERVERLESS_DOMAIN_REGEXP.match(uri) + return unless match + + cluster = ::Serverless::DomainCluster.find(match[:cluster_left] + match[:cluster_middle] + match[:cluster_right]) + return unless cluster + + environment = ::Environment.find(match[:environment_id].to_i(16)) + return unless environment&.slug == match[:environment_slug] + + new( + function: match[:function], + cluster: cluster, + environment: environment + ) + end + + private + + def initialize_required_argument(name, value) + raise ArgumentError.new("missing argument: #{name}") unless value + + instance_variable_set("@#{name}".to_sym, value) + end + end + end +end diff --git a/lib/gitlab/serverless/service.rb b/lib/gitlab/serverless/service.rb new file mode 100644 index 00000000000..643e076c587 --- /dev/null +++ b/lib/gitlab/serverless/service.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +class Gitlab::Serverless::Service + include Gitlab::Utils::StrongMemoize + + def initialize(attributes) + @attributes = attributes + end + + def name + @attributes.dig('metadata', 'name') + end + + def namespace + @attributes.dig('metadata', 'namespace') + end + + def environment_scope + @attributes.dig('environment_scope') + end + + def environment + @attributes.dig('environment') + end + + def podcount + @attributes.dig('podcount') + end + + def created_at + strong_memoize(:created_at) do + timestamp = @attributes.dig('metadata', 'creationTimestamp') + DateTime.parse(timestamp) if timestamp + end + end + + def image + @attributes.dig( + 'spec', + 'runLatest', + 'configuration', + 'build', + 'template', + 'name') + end + + def description + knative_07_description || knative_05_06_description + end + + def cluster + @attributes.dig('cluster') + end + + def url + proxy_url || knative_06_07_url || knative_05_url + end + + private + + def proxy_url + if cluster&.serverless_domain + Gitlab::Serverless::FunctionURI.new(function: name, cluster: cluster.serverless_domain, environment: environment) + end + end + + def knative_07_description + @attributes.dig( + 'spec', + 'template', + 'metadata', + 'annotations', + 'Description' + ) + end + + def knative_05_06_description + @attributes.dig( + 'spec', + 'runLatest', + 'configuration', + 'revisionTemplate', + 'metadata', + 'annotations', + 'Description') + end + + def knative_05_url + domain = @attributes.dig('status', 'domain') + return unless domain + + "http://#{domain}" + end + + def knative_06_07_url + @attributes.dig('status', 'url') + end +end diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 290c4cff329..726ecd81824 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -11,18 +11,26 @@ module Gitlab Error = Class.new(StandardError) class << self + # Retrieve GitLab Shell secret token + # + # @return [String] secret token def secret_token @secret_token ||= begin File.read(Gitlab.config.gitlab_shell.secret_file).chomp end end + # Ensure gitlab shell has a secret token stored in the secret_file + # if that was never generated, generate a new one def ensure_secret_token! return if File.exist?(File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret')) generate_and_link_secret_token end + # Returns required GitLab shell version + # + # @return [String] version from the manifest file def version_required @version_required ||= File.read(Rails.root .join('GITLAB_SHELL_VERSION')).strip @@ -48,24 +56,31 @@ module Gitlab end end - # Convenience methods for initializing a new repository with a Project model. + # Initialize a new project repository using a Project model + # + # @param [Project] project + # @return [Boolean] whether repository could be created def create_project_repository(project) create_repository(project.repository_storage, project.disk_path, project.full_path) end + # Initialize a new wiki repository using a Project model + # + # @param [Project] project + # @return [Boolean] whether repository could be created def create_wiki_repository(project) create_repository(project.repository_storage, project.wiki.disk_path, project.wiki.full_path) end # Init new repository # - # storage - the shard key - # disk_path - project disk path - # gl_project_path - project name - # - # Ex. + # @example Create a repository # create_repository("default", "path/to/gitlab-ci", "gitlab/gitlab-ci") # + # @param [String] storage the shard key + # @param [String] disk_path project path on disk + # @param [String] gl_project_path project name + # @return [Boolean] whether repository could be created def create_repository(storage, disk_path, gl_project_path) relative_path = disk_path.dup relative_path << '.git' unless relative_path.end_with?('.git') @@ -82,29 +97,39 @@ module Gitlab false end + # Import wiki repository from external service + # + # @param [Project] project + # @param [Gitlab::LegacyGithubImport::WikiFormatter, Gitlab::BitbucketImport::WikiFormatter] wiki_formatter + # @return [Boolean] whether repository could be imported def import_wiki_repository(project, wiki_formatter) import_repository(project.repository_storage, wiki_formatter.disk_path, wiki_formatter.import_url, project.wiki.full_path) end + # Import project repository from external service + # + # @param [Project] project + # @return [Boolean] whether repository could be imported def import_project_repository(project) import_repository(project.repository_storage, project.disk_path, project.import_url, project.full_path) end # Import repository # - # storage - project's storage name - # name - project disk path - # url - URL to import from - # - # Ex. - # import_repository("nfs-file06", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git") + # @example Import a repository + # import_repository("nfs-file06", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git", "gitlab/gitlab-ci") # - def import_repository(storage, name, url, gl_project_path) + # @param [String] storage project's storage name + # @param [String] disk_path project path on disk + # @param [String] url from external resource to import from + # @param [String] gl_project_path project name + # @return [Boolean] whether repository could be imported + def import_repository(storage, disk_path, url, gl_project_path) if url.start_with?('.', '/') raise Error.new("don't use disk paths with import_repository: #{url.inspect}") end - relative_path = "#{name}.git" + relative_path = "#{disk_path}.git" cmd = GitalyGitlabProjects.new(storage, relative_path, gl_project_path) success = cmd.import_project(url, git_timeout) @@ -113,27 +138,31 @@ module Gitlab success end - # storage - project's storage path - # path - project disk path - # new_path - new project disk path + # Move or rename a repository # - # Ex. + # @example Move/rename a repository # mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new") - def mv_repository(storage, path, new_path) - return false if path.empty? || new_path.empty? + # + # @param [String] storage project's storage path + # @param [String] disk_path current project path on disk + # @param [String] new_disk_path new project path on disk + # @return [Boolean] whether repository could be moved/renamed on disk + def mv_repository(storage, disk_path, new_disk_path) + return false if disk_path.empty? || new_disk_path.empty? - Gitlab::Git::Repository.new(storage, "#{path}.git", nil, nil).rename("#{new_path}.git") + Gitlab::Git::Repository.new(storage, "#{disk_path}.git", nil, nil).rename("#{new_disk_path}.git") true rescue => e - Gitlab::ErrorTracking.track_exception(e, path: path, new_path: new_path, storage: storage) + Gitlab::ErrorTracking.track_exception(e, path: disk_path, new_path: new_disk_path, storage: storage) false end # Fork repository to new path - # source_project - forked-from Project - # target_project - forked-to Project + # + # @param [Project] source_project forked-from Project + # @param [Project] target_project forked-to Project def fork_repository(source_project, target_project) forked_from_relative_path = "#{source_project.disk_path}.git" fork_args = [target_project.repository_storage, "#{target_project.disk_path}.git", target_project.full_path] @@ -145,29 +174,32 @@ module Gitlab # for rm_namespace. Given the underlying implementation removes the name # passed as second argument on the passed storage. # - # storage - project's storage path - # name - project disk path - # - # Ex. + # @example Remove a repository # remove_repository("/path/to/storage", "gitlab/gitlab-ci") - def remove_repository(storage, name) - return false if name.empty? + # + # @param [String] storage project's storage path + # @param [String] disk_path current project path on disk + def remove_repository(storage, disk_path) + return false if disk_path.empty? - Gitlab::Git::Repository.new(storage, "#{name}.git", nil, nil).remove + Gitlab::Git::Repository.new(storage, "#{disk_path}.git", nil, nil).remove true rescue => e - Rails.logger.warn("Repository does not exist: #{e} at: #{name}.git") # rubocop:disable Gitlab/RailsLogger - Gitlab::ErrorTracking.track_exception(e, path: name, storage: storage) + Rails.logger.warn("Repository does not exist: #{e} at: #{disk_path}.git") # rubocop:disable Gitlab/RailsLogger + Gitlab::ErrorTracking.track_exception(e, path: disk_path, storage: storage) false end # Add new key to authorized_keys # - # Ex. + # @example Add new key # add_key("key-42", "sha-rsa ...") # + # @param [String] key_id identifier of the key + # @param [String] key_content key content (public certificate) + # @return [Boolean] whether key could be added def add_key(key_id, key_content) return unless self.authorized_keys_enabled? @@ -176,39 +208,45 @@ module Gitlab # Batch-add keys to authorized_keys # - # Ex. + # @example # batch_add_keys(Key.all) + # + # @param [Array<Key>] keys + # @return [Boolean] whether keys could be added def batch_add_keys(keys) return unless self.authorized_keys_enabled? gitlab_authorized_keys.batch_add_keys(keys) end - # Remove ssh key from authorized_keys + # Remove SSH key from authorized_keys # - # Ex. + # @example Remove a key # remove_key("key-342") # - def remove_key(id, _ = nil) + # @param [String] key_id + # @return [Boolean] whether key could be removed or not + def remove_key(key_id, _ = nil) return unless self.authorized_keys_enabled? - gitlab_authorized_keys.rm_key(id) + gitlab_authorized_keys.rm_key(key_id) end - # Remove all ssh keys from gitlab shell + # Remove all SSH keys from gitlab shell # - # Ex. + # @example Remove all keys # remove_all_keys # + # @return [Boolean] whether keys could be removed or not def remove_all_keys return unless self.authorized_keys_enabled? gitlab_authorized_keys.clear end - # Remove ssh keys from gitlab shell that are not in the DB + # Remove SSH keys from gitlab shell that are not in the DB # - # Ex. + # @example Remove keys not on the database # remove_keys_not_found_in_db # # rubocop: disable CodeReuse/ActiveRecord @@ -234,11 +272,12 @@ module Gitlab # Add empty directory for storing repositories # - # Ex. + # @example Add new namespace directory # add_namespace("default", "gitlab") # + # @param [String] storage project's storage path + # @param [String] name namespace name def add_namespace(storage, name) - # https://gitlab.com/gitlab-org/gitlab-foss/issues/58012 Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient::NamespaceService.new(storage).add(name) end @@ -249,9 +288,11 @@ module Gitlab # Remove directory from repositories storage # Every repository inside this directory will be removed too # - # Ex. + # @example Remove namespace directory # rm_namespace("default", "gitlab") # + # @param [String] storage project's storage path + # @param [String] name namespace name def rm_namespace(storage, name) Gitlab::GitalyClient::NamespaceService.new(storage).remove(name) rescue GRPC::InvalidArgument => e @@ -261,9 +302,12 @@ module Gitlab # Move namespace directory inside repositories storage # - # Ex. + # @example Move/rename a namespace directory # mv_namespace("/path/to/storage", "gitlab", "gitlabhq") # + # @param [String] storage project's storage path + # @param [String] old_name current namespace name + # @param [String] new_name new namespace name def mv_namespace(storage, old_name, new_name) Gitlab::GitalyClient::NamespaceService.new(storage).rename(old_name, new_name) rescue GRPC::InvalidArgument => e @@ -272,11 +316,17 @@ module Gitlab false end - def url_to_repo(path) - Gitlab.config.gitlab_shell.ssh_path_prefix + "#{path}.git" + # Return a SSH url for a given project path + # + # @param [String] full_path project path (URL) + # @return [String] SSH URL + def url_to_repo(full_path) + Gitlab.config.gitlab_shell.ssh_path_prefix + "#{full_path}.git" end # Return GitLab shell version + # + # @return [String] version def version gitlab_shell_version_file = "#{gitlab_shell_path}/VERSION" @@ -285,12 +335,23 @@ module Gitlab end end + # Check if repository exists on disk + # + # @example Check if repository exists + # repository_exists?('default', 'gitlab-org/gitlab.git') + # + # @return [Boolean] whether repository exists or not + # @param [String] storage project's storage path + # @param [Object] dir_name repository dir name def repository_exists?(storage, dir_name) Gitlab::Git::Repository.new(storage, dir_name, nil, nil).exists? rescue GRPC::Internal false end + # Return hooks folder path used by projects + # + # @return [String] path def hooks_path File.join(gitlab_shell_path, 'hooks') end diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb index b246c507e9e..4e0d3da1868 100644 --- a/lib/gitlab/sidekiq_config.rb +++ b/lib/gitlab/sidekiq_config.rb @@ -4,6 +4,20 @@ require 'yaml' module Gitlab module SidekiqConfig + FOSS_QUEUE_CONFIG_PATH = 'app/workers/all_queues.yml' + EE_QUEUE_CONFIG_PATH = 'ee/app/workers/all_queues.yml' + SIDEKIQ_QUEUES_PATH = 'config/sidekiq_queues.yml' + + QUEUE_CONFIG_PATHS = [ + FOSS_QUEUE_CONFIG_PATH, + (EE_QUEUE_CONFIG_PATH if Gitlab.ee?) + ].compact.freeze + + DEFAULT_WORKERS = [ + DummyWorker.new('default', weight: 1), + DummyWorker.new('mailers', weight: 2) + ].map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: false) }.freeze + class << self include Gitlab::SidekiqConfig::CliMethods @@ -14,7 +28,7 @@ module Gitlab def config_queues @config_queues ||= begin - config = YAML.load_file(Rails.root.join('config/sidekiq_queues.yml')) + config = YAML.load_file(Rails.root.join(SIDEKIQ_QUEUES_PATH)) config[:queues].map(&:first) end end @@ -25,28 +39,69 @@ module Gitlab def workers @workers ||= begin - result = find_workers(Rails.root.join('app', 'workers')) - result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'))) if Gitlab.ee? + result = [] + result.concat(DEFAULT_WORKERS) + result.concat(find_workers(Rails.root.join('app', 'workers'), ee: false)) + + if Gitlab.ee? + result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'), ee: true)) + end + result end end + def workers_for_all_queues_yml + workers.partition(&:ee?).reverse.map(&:sort) + end + + # YAML.load_file is OK here as we control the file contents + def all_queues_yml_outdated? + foss_workers, ee_workers = workers_for_all_queues_yml + + return true if foss_workers != YAML.load_file(FOSS_QUEUE_CONFIG_PATH) + + Gitlab.ee? && ee_workers != YAML.load_file(EE_QUEUE_CONFIG_PATH) + end + + def queues_for_sidekiq_queues_yml + namespaces_with_equal_weights = + workers + .group_by(&:queue_namespace) + .map(&:last) + .select { |workers| workers.map(&:get_weight).uniq.count == 1 } + .map(&:first) + + namespaces = namespaces_with_equal_weights.map(&:queue_namespace).to_set + remaining_queues = workers.reject { |worker| namespaces.include?(worker.queue_namespace) } + + (namespaces_with_equal_weights.map(&:namespace_and_weight) + + remaining_queues.map(&:queue_and_weight)).sort + end + + # YAML.load_file is OK here as we control the file contents + def sidekiq_queues_yml_outdated? + config_queues = YAML.load_file(SIDEKIQ_QUEUES_PATH)[:queues] + + queues_for_sidekiq_queues_yml != config_queues + end + private - def find_workers(root) + def find_workers(root, ee:) concerns = root.join('concerns').to_s - workers = Dir[root.join('**', '*.rb')] + Dir[root.join('**', '*.rb')] .reject { |path| path.start_with?(concerns) } + .map { |path| worker_from_path(path, root) } + .select { |worker| worker < Sidekiq::Worker } + .map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: ee) } + end - workers.map! do |path| - ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '') - - ns.camelize.constantize - end + def worker_from_path(path, root) + ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '') - # Skip things that aren't workers - workers.select { |w| w < Sidekiq::Worker } + ns.camelize.constantize end end end diff --git a/lib/gitlab/sidekiq_config/cli_methods.rb b/lib/gitlab/sidekiq_config/cli_methods.rb index 1ce46289e81..8f19b557d24 100644 --- a/lib/gitlab/sidekiq_config/cli_methods.rb +++ b/lib/gitlab/sidekiq_config/cli_methods.rb @@ -18,7 +18,25 @@ module Gitlab result end.freeze - def worker_queues(rails_path = Rails.root.to_s) + QUERY_OR_OPERATOR = '|' + QUERY_AND_OPERATOR = '&' + QUERY_CONCATENATE_OPERATOR = ',' + QUERY_TERM_REGEX = %r{^(\w+)(!?=)([\w#{QUERY_CONCATENATE_OPERATOR}]+)}.freeze + + QUERY_PREDICATES = { + feature_category: :to_sym, + has_external_dependencies: lambda { |value| value == 'true' }, + latency_sensitive: lambda { |value| value == 'true' }, + name: :to_s, + resource_boundary: :to_sym + }.freeze + + QueryError = Class.new(StandardError) + InvalidTerm = Class.new(QueryError) + UnknownOperator = Class.new(QueryError) + UnknownPredicate = Class.new(QueryError) + + def all_queues(rails_path = Rails.root.to_s) @worker_queues ||= {} @worker_queues[rails_path] ||= QUEUE_CONFIG_PATHS.flat_map do |path| @@ -27,6 +45,12 @@ module Gitlab File.exist?(full_path) ? YAML.load_file(full_path) : [] end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + def worker_queues(rails_path = Rails.root.to_s) + # https://gitlab.com/gitlab-org/gitlab/issues/199230 + worker_names(all_queues(rails_path)) + end def expand_queues(queues, all_queues = self.worker_queues) return [] if queues.empty? @@ -37,7 +61,65 @@ module Gitlab [queue, *queues_set.grep(/\A#{queue}:/)] end end - # rubocop:enable Gitlab/ModuleWithInstanceVariables + + def query_workers(query_string, queues) + worker_names(queues.select(&query_string_to_lambda(query_string))) + end + + def clear_memoization! + if instance_variable_defined?('@worker_queues') + remove_instance_variable('@worker_queues') + end + end + + private + + def worker_names(workers) + workers.map { |queue| queue.is_a?(Hash) ? queue[:name] : queue } + end + + def query_string_to_lambda(query_string) + or_clauses = query_string.split(QUERY_OR_OPERATOR).map do |and_clauses_string| + and_clauses_predicates = and_clauses_string.split(QUERY_AND_OPERATOR).map do |term| + predicate_for_term(term) + end + + lambda { |worker| and_clauses_predicates.all? { |predicate| predicate.call(worker) } } + end + + lambda { |worker| or_clauses.any? { |predicate| predicate.call(worker) } } + end + + def predicate_for_term(term) + match = term.match(QUERY_TERM_REGEX) + + raise InvalidTerm.new("Invalid term: #{term}") unless match + + _, lhs, op, rhs = *match + + predicate_for_op(op, predicate_factory(lhs, rhs.split(QUERY_CONCATENATE_OPERATOR))) + end + + def predicate_for_op(op, predicate) + case op + when '=' + predicate + when '!=' + lambda { |worker| !predicate.call(worker) } + else + # This is unreachable because InvalidTerm will be raised instead, but + # keeping it allows to guard against that changing in future. + raise UnknownOperator.new("Unknown operator: #{op}") + end + end + + def predicate_factory(lhs, values) + values_block = QUERY_PREDICATES[lhs.to_sym] + + raise UnknownPredicate.new("Unknown predicate: #{lhs}") unless values_block + + lambda { |queue| values.map(&values_block).include?(queue[lhs.to_sym]) } + end end end end diff --git a/lib/gitlab/sidekiq_config/dummy_worker.rb b/lib/gitlab/sidekiq_config/dummy_worker.rb new file mode 100644 index 00000000000..858ff0db0c9 --- /dev/null +++ b/lib/gitlab/sidekiq_config/dummy_worker.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqConfig + # For queues that don't have explicit workers - default and mailers + class DummyWorker + attr_accessor :queue + + ATTRIBUTE_METHODS = { + feature_category: :get_feature_category, + has_external_dependencies: :worker_has_external_dependencies?, + latency_sensitive: :latency_sensitive_worker?, + resource_boundary: :get_worker_resource_boundary, + weight: :get_weight + }.freeze + + def initialize(queue, attributes = {}) + @queue = queue + @attributes = attributes + end + + def queue_namespace + nil + end + + ATTRIBUTE_METHODS.each do |attribute, meth| + define_method meth do + @attributes[attribute] + end + end + end + end +end diff --git a/lib/gitlab/sidekiq_config/worker.rb b/lib/gitlab/sidekiq_config/worker.rb new file mode 100644 index 00000000000..6cbe327e6b2 --- /dev/null +++ b/lib/gitlab/sidekiq_config/worker.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqConfig + class Worker + include Comparable + + attr_reader :klass + delegate :feature_category_not_owned?, :get_feature_category, + :get_weight, :get_worker_resource_boundary, + :latency_sensitive_worker?, :queue, :queue_namespace, + :worker_has_external_dependencies?, + to: :klass + + def initialize(klass, ee:) + @klass = klass + @ee = ee + end + + def ee? + @ee + end + + def ==(other) + to_yaml == case other + when self.class + other.to_yaml + else + other + end + end + + def <=>(other) + to_sort <=> other.to_sort + end + + # Put namespaced queues first + def to_sort + [queue_namespace ? 0 : 1, queue] + end + + # YAML representation + def encode_with(coder) + coder.represent_map(nil, to_yaml) + end + + def to_yaml + { + name: queue, + feature_category: get_feature_category, + has_external_dependencies: worker_has_external_dependencies?, + latency_sensitive: latency_sensitive_worker?, + resource_boundary: get_worker_resource_boundary, + weight: get_weight + } + end + + def namespace_and_weight + [queue_namespace, get_weight] + end + + def queue_and_weight + [queue, get_weight] + end + end + end +end diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb index 8e7626b8eb6..b45014d283f 100644 --- a/lib/gitlab/sidekiq_logging/structured_logger.rb +++ b/lib/gitlab/sidekiq_logging/structured_logger.rb @@ -6,8 +6,6 @@ require 'active_record/log_subscriber' module Gitlab module SidekiqLogging class StructuredLogger - MAXIMUM_JOB_ARGUMENTS_LENGTH = 10.kilobytes - def call(job, queue) started_time = get_time base_payload = parse_job(job) @@ -79,13 +77,15 @@ module Gitlab end def parse_job(job) - job = job.dup + # Error information from the previous try is in the payload for + # displaying in the Sidekiq UI, but is very confusing in logs! + job = job.except('error_backtrace', 'error_class', 'error_message') # Add process id params job['pid'] = ::Process.pid job.delete('args') unless ENV['SIDEKIQ_LOG_ARGUMENTS'] - job['args'] = limited_job_args(job['args']) if job['args'] + job['args'] = Gitlab::Utils::LogLimitedArray.log_limited_array(job['args']) if job['args'] job end @@ -108,21 +108,6 @@ module Gitlab def current_time Gitlab::Metrics::System.monotonic_time end - - def limited_job_args(args) - return unless args.is_a?(Array) - - total_length = 0 - limited_args = args.take_while do |arg| - total_length += arg.to_json.length - - total_length <= MAXIMUM_JOB_ARGUMENTS_LENGTH - end - - limited_args.push('...') if total_length > MAXIMUM_JOB_ARGUMENTS_LENGTH - - limited_args - end end end end diff --git a/lib/gitlab/sidekiq_middleware.rb b/lib/gitlab/sidekiq_middleware.rb index 3dda244233f..6c27213df49 100644 --- a/lib/gitlab/sidekiq_middleware.rb +++ b/lib/gitlab/sidekiq_middleware.rb @@ -17,7 +17,9 @@ module Gitlab chain.add Gitlab::SidekiqMiddleware::BatchLoader chain.add Labkit::Middleware::Sidekiq::Server chain.add Gitlab::SidekiqMiddleware::InstrumentationLogger + chain.add Gitlab::SidekiqMiddleware::AdminMode::Server chain.add Gitlab::SidekiqStatus::ServerMiddleware + chain.add Gitlab::SidekiqMiddleware::WorkerContext::Server end end @@ -28,7 +30,9 @@ module Gitlab lambda do |chain| chain.add Gitlab::SidekiqStatus::ClientMiddleware chain.add Gitlab::SidekiqMiddleware::ClientMetrics + chain.add Gitlab::SidekiqMiddleware::WorkerContext::Client # needs to be before the Labkit middleware chain.add Labkit::Middleware::Sidekiq::Client + chain.add Gitlab::SidekiqMiddleware::AdminMode::Client end end end diff --git a/lib/gitlab/sidekiq_middleware/admin_mode/client.rb b/lib/gitlab/sidekiq_middleware/admin_mode/client.rb new file mode 100644 index 00000000000..e227ee654ee --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/admin_mode/client.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqMiddleware + module AdminMode + # Checks if admin mode is enabled for the request creating the sidekiq job + # by examining if admin mode has been enabled for the user + # If enabled then it injects a job field that persists through the job execution + class Client + def call(_worker_class, job, _queue, _redis_pool) + return yield unless Feature.enabled?(:user_mode_in_session) + + # Admin mode enabled in the original request or in a nested sidekiq job + admin_mode_user_id = find_admin_user_id + + if admin_mode_user_id + job['admin_mode_user_id'] ||= admin_mode_user_id + + Gitlab::AppLogger.debug("AdminMode::Client injected admin mode for job: #{job.inspect}") + end + + yield + end + + private + + def find_admin_user_id + Gitlab::Auth::CurrentUserMode.current_admin&.id || + Gitlab::Auth::CurrentUserMode.bypass_session_admin_id + end + end + end + end +end diff --git a/lib/gitlab/sidekiq_middleware/admin_mode/server.rb b/lib/gitlab/sidekiq_middleware/admin_mode/server.rb new file mode 100644 index 00000000000..6366867a0fa --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/admin_mode/server.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqMiddleware + module AdminMode + class Server + def call(_worker, job, _queue) + return yield unless Feature.enabled?(:user_mode_in_session) + + admin_mode_user_id = job['admin_mode_user_id'] + + # Do not bypass session if this job was not enabled with admin mode on + return yield unless admin_mode_user_id + + Gitlab::Auth::CurrentUserMode.bypass_session!(admin_mode_user_id) do + Gitlab::AppLogger.debug("AdminMode::Server bypasses session for admin mode in job: #{job.inspect}") + + yield + end + end + end + end + end +end diff --git a/lib/gitlab/sidekiq_middleware/client_metrics.rb b/lib/gitlab/sidekiq_middleware/client_metrics.rb index cd11415b55e..245a1b5e024 100644 --- a/lib/gitlab/sidekiq_middleware/client_metrics.rb +++ b/lib/gitlab/sidekiq_middleware/client_metrics.rb @@ -9,8 +9,10 @@ module Gitlab @metrics = init_metrics end - def call(worker, _job, queue, _redis_pool) - labels = create_labels(worker.class, queue) + def call(worker_class, _job, queue, _redis_pool) + # worker_class can either be the string or class of the worker being enqueued. + worker_class = worker_class.safe_constantize if worker_class.respond_to?(:safe_constantize) + labels = create_labels(worker_class, queue) @metrics.fetch(ENQUEUED).increment(labels, 1) diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb index 9588e9ef19a..fbc34357323 100644 --- a/lib/gitlab/sidekiq_middleware/metrics.rb +++ b/lib/gitlab/sidekiq_middleware/metrics.rb @@ -10,7 +10,7 @@ module Gitlab def create_labels(worker_class, queue) labels = { queue: queue.to_s, latency_sensitive: FALSE_LABEL, external_dependencies: FALSE_LABEL, feature_category: "", boundary: "" } - return labels unless worker_class.include? WorkerAttributes + return labels unless worker_class && worker_class.include?(WorkerAttributes) labels[:latency_sensitive] = bool_as_label(worker_class.latency_sensitive_worker?) labels[:external_dependencies] = bool_as_label(worker_class.worker_has_external_dependencies?) diff --git a/lib/gitlab/sidekiq_middleware/worker_context.rb b/lib/gitlab/sidekiq_middleware/worker_context.rb new file mode 100644 index 00000000000..897a9211948 --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/worker_context.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqMiddleware + module WorkerContext + private + + def wrap_in_optional_context(context_or_nil, &block) + return yield unless context_or_nil + + context_or_nil.use(&block) + end + end + end +end diff --git a/lib/gitlab/sidekiq_middleware/worker_context/client.rb b/lib/gitlab/sidekiq_middleware/worker_context/client.rb new file mode 100644 index 00000000000..0eb52179db2 --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/worker_context/client.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqMiddleware + module WorkerContext + class Client + include Gitlab::SidekiqMiddleware::WorkerContext + + def call(worker_class_or_name, job, _queue, _redis_pool, &block) + worker_class = worker_class_or_name.to_s.safe_constantize + + # Mailers can't be constantized like this + return yield unless worker_class + return yield unless worker_class.include?(::ApplicationWorker) + + context_for_args = worker_class.context_for_arguments(job['args']) + + wrap_in_optional_context(context_for_args, &block) + end + end + end + end +end diff --git a/lib/gitlab/sidekiq_middleware/worker_context/server.rb b/lib/gitlab/sidekiq_middleware/worker_context/server.rb new file mode 100644 index 00000000000..d2d84742c17 --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/worker_context/server.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqMiddleware + module WorkerContext + class Server + include Gitlab::SidekiqMiddleware::WorkerContext + + def call(worker, job, _queue, &block) + worker_class = worker.class + + # This is not a worker we know about, perhaps from a gem + return yield unless worker_class.respond_to?(:get_worker_context) + + # Use the context defined on the class level as a base context + wrap_in_optional_context(worker_class.get_worker_context, &block) + end + end + end + end +end diff --git a/lib/gitlab/signed_commit.rb b/lib/gitlab/signed_commit.rb new file mode 100644 index 00000000000..809e0a3f034 --- /dev/null +++ b/lib/gitlab/signed_commit.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Gitlab + class SignedCommit + include Gitlab::Utils::StrongMemoize + + def initialize(commit) + @commit = commit + + if commit.project + repo = commit.project.repository.raw_repository + @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id) + end + + lazy_signature + end + + def signature + return unless @commit.has_signature? + end + + def signature_text + strong_memoize(:signature_text) do + @signature_data.itself ? @signature_data[0] : nil + end + end + + def signed_text + strong_memoize(:signed_text) do + @signature_data.itself ? @signature_data[1] : nil + end + end + end +end diff --git a/lib/gitlab/tab_width.rb b/lib/gitlab/tab_width.rb new file mode 100644 index 00000000000..d33723a2106 --- /dev/null +++ b/lib/gitlab/tab_width.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module TabWidth + extend self + + MIN = 1 + MAX = 12 + DEFAULT = 8 + + def css_class_for_user(user) + return css_class_for_value(DEFAULT) unless user + + css_class_for_value(user.tab_width) + end + + private + + def css_class_for_value(value) + raise ArgumentError unless in_range?(value) + + "tab-width-#{value}" + end + + def in_range?(value) + (MIN..MAX).cover?(value) + end + end +end diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb index 14c9a3e0389..50e09bdcdd6 100644 --- a/lib/gitlab/template/base_template.rb +++ b/lib/gitlab/template/base_template.rb @@ -33,7 +33,7 @@ module Gitlab self end - def to_json + def to_json(*) { key: key, name: name, content: content } end diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb index 63860b9cb26..5992f24f4e9 100644 --- a/lib/gitlab/themes.rb +++ b/lib/gitlab/themes.rb @@ -77,6 +77,10 @@ module Gitlab end end + def self.valid_ids + THEMES.map(&:id) + end + private def default_id diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index e00b49b9042..6e29a3e4cc4 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -67,8 +67,8 @@ module Gitlab clusters_disabled: count(::Clusters::Cluster.disabled), project_clusters_disabled: count(::Clusters::Cluster.disabled.project_type), group_clusters_disabled: count(::Clusters::Cluster.disabled.group_type), - clusters_platforms_eks: count(::Clusters::Cluster.aws_installed.enabled), - clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled), + clusters_platforms_eks: count(::Clusters::Cluster.aws_installed.enabled, batch: false), + clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled, batch: false), clusters_platforms_user: count(::Clusters::Cluster.user_provided.enabled), clusters_applications_helm: count(::Clusters::Applications::Helm.available), clusters_applications_ingress: count(::Clusters::Applications::Ingress.available), @@ -78,14 +78,16 @@ module Gitlab clusters_applications_runner: count(::Clusters::Applications::Runner.available), clusters_applications_knative: count(::Clusters::Applications::Knative.available), clusters_applications_elastic_stack: count(::Clusters::Applications::ElasticStack.available), + clusters_applications_jupyter: count(::Clusters::Applications::Jupyter.available), in_review_folder: count(::Environment.in_review_folder), grafana_integrated_projects: count(GrafanaIntegration.enabled), groups: count(Group), issues: count(Issue), issues_created_from_gitlab_error_tracking_ui: count(SentryIssue), issues_with_associated_zoom_link: count(ZoomMeeting.added_to_issue), - issues_using_zoom_quick_actions: count(ZoomMeeting.select(:issue_id).distinct), + issues_using_zoom_quick_actions: count(ZoomMeeting.select(:issue_id).distinct, batch: false), issues_with_embedded_grafana_charts_approx: ::Gitlab::GrafanaEmbedUsageData.issue_count, + incident_issues: count(::Issue.authored(::User.alert_bot)), keys: count(Key), label_lists: count(List.label), lfs_objects: count(LfsObject), @@ -97,6 +99,7 @@ module Gitlab projects_imported_from_github: count(Project.where(import_type: 'github')), projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)), projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)), + projects_with_alerts_service_enabled: count(AlertsService.active, batch: false), protected_branches: count(ProtectedBranch), releases: count(Release), remote_mirrors: count(RemoteMirror), @@ -178,7 +181,7 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def services_usage - service_counts = count(Service.active.where(template: false).where.not(type: 'JiraService').group(:type), fallback: Hash.new(-1)) + service_counts = count(Service.active.where(template: false).where.not(type: 'JiraService').group(:type), fallback: Hash.new(-1), batch: false) results = Service.available_services_names.each_with_object({}) do |service_name, response| response["projects_#{service_name}_active".to_sym] = service_counts["#{service_name}_service".camelize] || 0 @@ -214,9 +217,9 @@ module Gitlab results[:projects_jira_server_active] += counts[:server].count if counts[:server] results[:projects_jira_cloud_active] += counts[:cloud].count if counts[:cloud] if results[:projects_jira_active] == -1 - results[:projects_jira_active] = count(services) + results[:projects_jira_active] = count(services, batch: false) else - results[:projects_jira_active] += count(services) + results[:projects_jira_active] += count(services, batch: false) end end @@ -228,8 +231,22 @@ module Gitlab {} # augmented in EE end - def count(relation, fallback: -1) - relation.count + def count(relation, column = nil, fallback: -1, batch: true) + if batch && Feature.enabled?(:usage_ping_batch_counter) + Gitlab::Database::BatchCount.batch_count(relation, column) + else + relation.count + end + rescue ActiveRecord::StatementInvalid + fallback + end + + def distinct_count(relation, column = nil, fallback: -1, batch: true) + if batch && Feature.enabled?(:usage_ping_batch_counter) + Gitlab::Database::BatchCount.batch_distinct_count(relation, column) + else + relation.distinct_count_by(column) + end rescue ActiveRecord::StatementInvalid fallback end diff --git a/lib/gitlab/utils/deep_size.rb b/lib/gitlab/utils/deep_size.rb index ed2ceb8af7c..e185786e638 100644 --- a/lib/gitlab/utils/deep_size.rb +++ b/lib/gitlab/utils/deep_size.rb @@ -13,8 +13,8 @@ module Gitlab def initialize(root, max_size: DEFAULT_MAX_SIZE, max_depth: DEFAULT_MAX_DEPTH) @root = root - @max_size = max_size - @max_depth = max_depth + @max_size = max_size || DEFAULT_MAX_SIZE + @max_depth = max_depth || DEFAULT_MAX_DEPTH @size = 0 @depth = 0 diff --git a/lib/gitlab/utils/log_limited_array.rb b/lib/gitlab/utils/log_limited_array.rb new file mode 100644 index 00000000000..fe8aadf9020 --- /dev/null +++ b/lib/gitlab/utils/log_limited_array.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Utils + module LogLimitedArray + MAXIMUM_ARRAY_LENGTH = 10.kilobytes + + # Prepare an array for logging by limiting its JSON representation + # to around 10 kilobytes. Once we hit the limit, add "..." as the + # last item in the returned array. + def self.log_limited_array(array) + return [] unless array.is_a?(Array) + + total_length = 0 + limited_array = array.take_while do |arg| + total_length += arg.to_json.length + + total_length <= MAXIMUM_ARRAY_LENGTH + end + + limited_array.push('...') if total_length > MAXIMUM_ARRAY_LENGTH + + limited_array + end + end + end +end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 29450a33289..8696e23cbc7 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -24,7 +24,7 @@ module Gitlab attrs = { GL_ID: Gitlab::GlId.gl_id(user), - GL_REPOSITORY: repo_type.identifier_for_subject(repository.project), + GL_REPOSITORY: repo_type.identifier_for_container(repository.project), GL_USERNAME: user&.username, ShowAllRefs: show_all_refs, Repository: repository.gitaly_repository.to_h, diff --git a/lib/gitlab/x509/commit.rb b/lib/gitlab/x509/commit.rb new file mode 100644 index 00000000000..ce298b80a4c --- /dev/null +++ b/lib/gitlab/x509/commit.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true +require 'openssl' +require 'digest' + +module Gitlab + module X509 + class Commit < Gitlab::SignedCommit + def signature + super + + return @signature if @signature + + cached_signature = lazy_signature&.itself + return @signature = cached_signature if cached_signature.present? + + @signature = create_cached_signature! + end + + def update_signature!(cached_signature) + cached_signature.update!(attributes) + @signature = cached_signature + end + + private + + def lazy_signature + BatchLoader.for(@commit.sha).batch do |shas, loader| + X509CommitSignature.by_commit_sha(shas).each do |signature| + loader.call(signature.commit_sha, signature) + end + end + end + + def verified_signature + strong_memoize(:verified_signature) { verified_signature? } + end + + def cert + strong_memoize(:cert) do + signer_certificate(p7) if valid_signature? + end + end + + def cert_store + strong_memoize(:cert_store) do + store = OpenSSL::X509::Store.new + store.set_default_paths + # valid_signing_time? checks the time attributes already + # this flag is required, otherwise expired certificates would become + # unverified when notAfter within certificate attribute is reached + store.flags = OpenSSL::X509::V_FLAG_NO_CHECK_TIME + store + end + end + + def p7 + strong_memoize(:p7) do + pkcs7_text = signature_text.sub('-----BEGIN SIGNED MESSAGE-----', '-----BEGIN PKCS7-----') + pkcs7_text = pkcs7_text.sub('-----END SIGNED MESSAGE-----', '-----END PKCS7-----') + + OpenSSL::PKCS7.new(pkcs7_text) + rescue + nil + end + end + + def valid_signing_time? + # rfc 5280 - 4.1.2.5 Validity + # check if signed_time is within the time range (notBefore/notAfter) + # non-rfc - git specific check: signed_time >= commit_time + p7.signers[0].signed_time.between?(cert.not_before, cert.not_after) && + p7.signers[0].signed_time >= @commit.created_at + end + + def valid_signature? + p7.verify([], cert_store, signed_text, OpenSSL::PKCS7::NOVERIFY) + rescue + nil + end + + def verified_signature? + # verify has multiple options but only a boolean return value + # so first verify without certificate chain + if valid_signature? + if valid_signing_time? + # verify with system certificate chain + p7.verify([], cert_store, signed_text) + else + false + end + else + nil + end + rescue + nil + end + + def signer_certificate(p7) + p7.certificates.each do |cert| + next if cert.serial != p7.signers[0].serial + + return cert + end + end + + def certificate_crl + extension = get_certificate_extension('crlDistributionPoints') + extension.split('URI:').each do |item| + item.strip + + if item.start_with?("http") + return item.strip + end + end + end + + def get_certificate_extension(extension) + cert.extensions.each do |ext| + if ext.oid == extension + return ext.value + end + end + end + + def issuer_subject_key_identifier + get_certificate_extension('authorityKeyIdentifier').gsub("keyid:", "").delete!("\n") + end + + def certificate_subject_key_identifier + get_certificate_extension('subjectKeyIdentifier') + end + + def certificate_issuer + cert.issuer.to_s(OpenSSL::X509::Name::RFC2253) + end + + def certificate_subject + cert.subject.to_s(OpenSSL::X509::Name::RFC2253) + end + + def certificate_email + get_certificate_extension('subjectAltName').split('email:')[1] + end + + def issuer_attributes + return if verified_signature.nil? + + { + subject_key_identifier: issuer_subject_key_identifier, + subject: certificate_issuer, + crl_url: certificate_crl + } + end + + def certificate_attributes + return if verified_signature.nil? + + issuer = X509Issuer.safe_create!(issuer_attributes) + + { + subject_key_identifier: certificate_subject_key_identifier, + subject: certificate_subject, + email: certificate_email, + serial_number: cert.serial, + x509_issuer_id: issuer.id + } + end + + def attributes + return if verified_signature.nil? + + certificate = X509Certificate.safe_create!(certificate_attributes) + + { + commit_sha: @commit.sha, + project: @commit.project, + x509_certificate_id: certificate.id, + verification_status: verification_status + } + end + + def verification_status + if verified_signature && certificate_email == @commit.committer_email + :verified + else + :unverified + end + end + + def create_cached_signature! + return if verified_signature.nil? + + return X509CommitSignature.new(attributes) if Gitlab::Database.read_only? + + X509CommitSignature.safe_create!(attributes) + end + end + end +end diff --git a/lib/gitlab_danger.rb b/lib/gitlab_danger.rb index 499ae6111d7..e776e2b7ea3 100644 --- a/lib/gitlab_danger.rb +++ b/lib/gitlab_danger.rb @@ -18,7 +18,6 @@ class GitlabDanger changelog specs roulette - single_codebase gitlab_ui_wg ce_ee_vue_templates ].freeze diff --git a/lib/microsoft_teams/notifier.rb b/lib/microsoft_teams/notifier.rb index 340bf709f5e..096e1e2ee96 100644 --- a/lib/microsoft_teams/notifier.rb +++ b/lib/microsoft_teams/notifier.rb @@ -36,10 +36,7 @@ module MicrosoftTeams attachments = options[:attachments] unless attachments.blank? - result['sections'] << { - 'title' => 'Details', - 'facts' => [{ 'name' => 'Attachments', 'value' => attachments }] - } + result['sections'] << { text: attachments } end result.to_json diff --git a/lib/quality/kubernetes_client.rb b/lib/quality/kubernetes_client.rb index db21c0b013b..453b9d21adb 100644 --- a/lib/quality/kubernetes_client.rb +++ b/lib/quality/kubernetes_client.rb @@ -63,7 +63,7 @@ module Quality 'get', RESOURCE_LIST, %(--namespace "#{namespace}"), - '-o custom-columns=NAME:.metadata.name' + '-o name' ] run_command(command).lines.map(&:strip) end diff --git a/lib/quality/test_level.rb b/lib/quality/test_level.rb index 84470a73b1b..85e89059dbb 100644 --- a/lib/quality/test_level.rb +++ b/lib/quality/test_level.rb @@ -27,11 +27,13 @@ module Quality policies presenters rack_servers + replicators routing rubocop serializers services sidekiq + support_specs tasks uploaders validators diff --git a/lib/rspec_flaky/report.rb b/lib/rspec_flaky/report.rb index 9a0fb88c424..73f30362cfe 100644 --- a/lib/rspec_flaky/report.rb +++ b/lib/rspec_flaky/report.rb @@ -10,7 +10,7 @@ module RspecFlaky # This class is responsible for loading/saving JSON reports, and pruning # outdated examples. class Report < SimpleDelegator - OUTDATED_DAYS_THRESHOLD = 90 + OUTDATED_DAYS_THRESHOLD = 7 attr_reader :flaky_examples diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb index 8898960c24d..36ec1caf80c 100644 --- a/lib/sentry/client.rb +++ b/lib/sentry/client.rb @@ -54,6 +54,12 @@ module Sentry end end + def http_post(url, params = {}) + http_request do + Gitlab::HTTP.post(url, **request_params.merge(body: params.to_json)) + end + end + def http_request response = handle_request_exceptions do yield diff --git a/lib/sentry/client/issue_link.rb b/lib/sentry/client/issue_link.rb index 200b1a6b435..91498c19f8b 100644 --- a/lib/sentry/client/issue_link.rb +++ b/lib/sentry/client/issue_link.rb @@ -3,8 +3,22 @@ module Sentry class Client module IssueLink - def create_issue_link(integration_id, sentry_issue_identifier, issue) - issue_link_url = issue_link_api_url(integration_id, sentry_issue_identifier) + # Creates a link in Sentry corresponding to the provided + # Sentry issue and GitLab issue + # @param integration_id [Integer, nil] Representing a global + # GitLab integration in Sentry. Nil for plugins. + # @param sentry_issue_id [Integer] Id for an issue from Sentry + # @param issue [Issue] Issue for which the link should be created + def create_issue_link(integration_id, sentry_issue_id, issue) + return create_plugin_link(sentry_issue_id, issue) unless integration_id + + create_global_integration_link(integration_id, sentry_issue_id, issue) + end + + private + + def create_global_integration_link(integration_id, sentry_issue_id, issue) + issue_link_url = global_integration_link_api_url(integration_id, sentry_issue_id) params = { project: issue.project.id, @@ -14,11 +28,22 @@ module Sentry http_put(issue_link_url, params) end - private + def global_integration_link_api_url(integration_id, sentry_issue_id) + issue_link_url = URI(url) + issue_link_url.path = "/api/0/groups/#{sentry_issue_id}/integrations/#{integration_id}/" + + issue_link_url + end + + def create_plugin_link(sentry_issue_id, issue) + issue_link_url = plugin_link_api_url(sentry_issue_id) + + http_post(issue_link_url, issue_id: issue.iid) + end - def issue_link_api_url(integration_id, sentry_issue_identifier) + def plugin_link_api_url(sentry_issue_id) issue_link_url = URI(url) - issue_link_url.path = "/api/0/groups/#{sentry_issue_identifier}/integrations/#{integration_id}/" + issue_link_url.path = "/api/0/issues/#{sentry_issue_id}/plugins/gitlab/link/" issue_link_url end diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index bccb94ff0bf..1c51288adf6 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -57,7 +57,7 @@ gitaly_log="$app_root/log/gitaly.log" # Read configuration variable file if it is present test -f /etc/default/gitlab && . /etc/default/gitlab -# Switch to the app_user if it is not he/she who is running the script. +# Switch to the app_user if it is not they who are running the script. if [ `whoami` != "$app_user" ]; then eval su - "$app_user" -c $(echo \")$shell_path -l -c \'$0 "$@"\'$(echo \"); exit; fi @@ -340,7 +340,7 @@ start_gitlab() { # Wait for the pids to be planted wait_for_pids - # Finally check the status to tell wether or not GitLab is running + # Finally check the status to tell whether or not GitLab is running print_status } diff --git a/lib/system_check/helpers.rb b/lib/system_check/helpers.rb index 07d479848fe..8fbfe7af713 100644 --- a/lib/system_check/helpers.rb +++ b/lib/system_check/helpers.rb @@ -57,11 +57,7 @@ module SystemCheck end def should_sanitize? - if ENV['SANITIZE'] == 'true' - true - else - false - end + ENV['SANITIZE'] == 'true' end def omnibus_gitlab? diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake index cb4d5abffbc..c380eb293b5 100644 --- a/lib/tasks/cache.rake +++ b/lib/tasks/cache.rake @@ -3,7 +3,7 @@ namespace :cache do REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000 REDIS_SCAN_START_STOP = '0'.freeze # Magic value, see http://redis.io/commands/scan - desc "GitLab | Clear redis cache" + desc "GitLab | Cache | Clear redis cache" task redis: :environment do Gitlab::Redis::Cache.with do |redis| cache_key_pattern = %W[#{Gitlab::Redis::Cache::CACHE_NAMESPACE}* diff --git a/lib/tasks/ci/cleanup.rake b/lib/tasks/ci/cleanup.rake index 2f4d11bd942..978a42be638 100644 --- a/lib/tasks/ci/cleanup.rake +++ b/lib/tasks/ci/cleanup.rake @@ -1,6 +1,6 @@ namespace :ci do namespace :cleanup do - desc "GitLab CI | Clean running builds" + desc "GitLab | CI | Clean running builds" task builds: :environment do Ci::Build.running.update_all(status: 'canceled') end diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index 0488f26318a..b3ba2434855 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -1,7 +1,7 @@ task dev: ["dev:setup"] namespace :dev do - desc "GitLab | Setup developer environment (db, fixtures)" + desc "GitLab | Dev | Setup developer environment (db, fixtures)" task setup: :environment do ENV['force'] = 'yes' Rake::Task["gitlab:setup"].invoke @@ -12,7 +12,7 @@ namespace :dev do Rake::Task["gitlab:shell:setup"].invoke end - desc "GitLab | Eager load application" + desc "GitLab | Dev | Eager load application" task load: :environment do Rails.configuration.eager_load = true Rails.application.eager_load! diff --git a/lib/tasks/gitlab/artifacts/migrate.rake b/lib/tasks/gitlab/artifacts/migrate.rake index 0d09fd0a4e3..871fdfb4fde 100644 --- a/lib/tasks/gitlab/artifacts/migrate.rake +++ b/lib/tasks/gitlab/artifacts/migrate.rake @@ -1,7 +1,7 @@ require 'logger' require 'resolv-replace' -desc "GitLab | Migrate files for artifacts to comply with new storage format" +desc 'GitLab | Artifacts | Migrate files for artifacts to comply with new storage format' namespace :gitlab do namespace :artifacts do task migrate: :environment do diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index 3aa1dc403d6..b398bbe403f 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -8,7 +8,6 @@ namespace :gitlab do yarn:check gettext:po_to_json rake:assets:precompile - gitlab:assets:vendor webpack:compile gitlab:assets:fix_urls ].each(&Gitlab::TaskHelpers.method(:invoke_and_time_task)) diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 2bf71701b57..8f34101ea15 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -3,18 +3,18 @@ require 'active_record/fixtures' namespace :gitlab do namespace :backup do # Create backup of GitLab system - desc "GitLab | Create a backup of the GitLab system" + desc 'GitLab | Backup | Create a backup of the GitLab system' task create: :gitlab_environment do warn_user_is_not_gitlab - Rake::Task["gitlab:backup:db:create"].invoke - Rake::Task["gitlab:backup:repo:create"].invoke - Rake::Task["gitlab:backup:uploads:create"].invoke - Rake::Task["gitlab:backup:builds:create"].invoke - Rake::Task["gitlab:backup:artifacts:create"].invoke - Rake::Task["gitlab:backup:pages:create"].invoke - Rake::Task["gitlab:backup:lfs:create"].invoke - Rake::Task["gitlab:backup:registry:create"].invoke + Rake::Task['gitlab:backup:db:create'].invoke + Rake::Task['gitlab:backup:repo:create'].invoke + Rake::Task['gitlab:backup:uploads:create'].invoke + Rake::Task['gitlab:backup:builds:create'].invoke + Rake::Task['gitlab:backup:artifacts:create'].invoke + Rake::Task['gitlab:backup:pages:create'].invoke + Rake::Task['gitlab:backup:lfs:create'].invoke + Rake::Task['gitlab:backup:registry:create'].invoke backup = Backup::Manager.new(progress) backup.pack @@ -28,7 +28,7 @@ namespace :gitlab do end # Restore backup of GitLab system - desc 'GitLab | Restore a previously created backup' + desc 'GitLab | Backup | Restore a previously created backup' task restore: :gitlab_environment do warn_user_is_not_gitlab diff --git a/lib/tasks/gitlab/bulk_add_permission.rake b/lib/tasks/gitlab/bulk_add_permission.rake index c0d6cc8ca8e..56cbbae1f67 100644 --- a/lib/tasks/gitlab/bulk_add_permission.rake +++ b/lib/tasks/gitlab/bulk_add_permission.rake @@ -1,6 +1,6 @@ namespace :gitlab do namespace :import do - desc "GitLab | Add all users to all projects (admin users are added as maintainers)" + desc "GitLab | Import | Add all users to all projects (admin users are added as maintainers)" task all_users_to_all_projects: :environment do |t, args| user_ids = User.where(admin: false).pluck(:id) admin_ids = User.where(admin: true).pluck(:id) @@ -13,7 +13,7 @@ namespace :gitlab do ProjectMember.add_users_to_projects(project_ids, admin_ids, ProjectMember::MAINTAINER) end - desc "GitLab | Add a specific user to all projects (as a developer)" + desc "GitLab | Import | Add a specific user to all projects (as a developer)" task :user_to_projects, [:email] => :environment do |t, args| user = User.find_by(email: args.email) project_ids = Project.pluck(:id) @@ -21,7 +21,7 @@ namespace :gitlab do ProjectMember.add_users_to_projects(project_ids, Array.wrap(user.id), ProjectMember::DEVELOPER) end - desc "GitLab | Add all users to all groups (admin users are added as owners)" + desc "GitLab | Import | Add all users to all groups (admin users are added as owners)" task all_users_to_all_groups: :environment do |t, args| user_ids = User.where(admin: false).pluck(:id) admin_ids = User.where(admin: true).pluck(:id) @@ -35,7 +35,7 @@ namespace :gitlab do end end - desc "GitLab | Add a specific user to all groups (as a developer)" + desc "GitLab | Import | Add a specific user to all groups (as a developer)" task :user_to_groups, [:email] => :environment do |t, args| user = User.find_by_email args.email groups = Group.all diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index b594f150c3b..9e60a585330 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -5,35 +5,35 @@ namespace :gitlab do end namespace :app do - desc 'GitLab | Check the configuration of the GitLab Rails app' + desc 'GitLab | App | Check the configuration of the GitLab Rails app' task check: :gitlab_environment do SystemCheck::RakeTask::AppTask.run! end end namespace :gitlab_shell do - desc "GitLab | Check the configuration of GitLab Shell" + desc 'GitLab | GitLab Shell | Check the configuration of GitLab Shell' task check: :gitlab_environment do SystemCheck::RakeTask::GitlabShellTask.run! end end namespace :gitaly do - desc 'GitLab | Check the health of Gitaly' + desc 'GitLab | Gitaly | Check the health of Gitaly' task check: :gitlab_environment do SystemCheck::RakeTask::GitalyTask.run! end end namespace :sidekiq do - desc "GitLab | Check the configuration of Sidekiq" + desc 'GitLab | Sidekiq | Check the configuration of Sidekiq' task check: :gitlab_environment do SystemCheck::RakeTask::SidekiqTask.run! end end namespace :incoming_email do - desc "GitLab | Check the configuration of Reply by email" + desc 'GitLab | Incoming Email | Check the configuration of Reply by email' task check: :gitlab_environment do SystemCheck::RakeTask::IncomingEmailTask.run! end @@ -48,17 +48,17 @@ namespace :gitlab do end namespace :orphans do - desc 'Gitlab | Check for orphaned namespaces and repositories' + desc 'Gitlab | Orphans | Check for orphaned namespaces and repositories' task check: :gitlab_environment do SystemCheck::RakeTask::OrphansTask.run! end - desc 'GitLab | Check for orphaned namespaces in the repositories path' + desc 'GitLab | Orphans | Check for orphaned namespaces in the repositories path' task check_namespaces: :gitlab_environment do SystemCheck::RakeTask::Orphans::NamespaceTask.run! end - desc 'GitLab | Check for orphaned repositories in the repositories path' + desc 'GitLab | Orphans | Check for orphaned repositories in the repositories path' task check_repositories: :gitlab_environment do SystemCheck::RakeTask::Orphans::RepositoryTask.run! end diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 1961f64659c..e72c5f51ada 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -1,6 +1,6 @@ namespace :gitlab do namespace :db do - desc 'GitLab | Manually insert schema migration version' + desc 'GitLab | DB | Manually insert schema migration version' task :mark_migration_complete, [:version] => :environment do |_, args| unless args[:version] puts "Must specify a migration version as an argument".color(:red) @@ -22,7 +22,7 @@ namespace :gitlab do end end - desc 'Drop all tables' + desc 'GitLab | DB | Drop all tables' task drop_tables: :environment do connection = ActiveRecord::Base.connection @@ -41,7 +41,7 @@ namespace :gitlab do tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{connection.quote_table_name(t)} CASCADE") } end - desc 'Configures the database by running migrate, or by loading the schema and seeding if needed' + desc 'GitLab | DB | Configures the database by running migrate, or by loading the schema and seeding if needed' task configure: :environment do # Check if we have existing db tables # The schema_migrations table will still exist if drop_tables was called @@ -55,7 +55,7 @@ namespace :gitlab do end end - desc 'Checks if migrations require downtime or not' + desc 'GitLab | DB | Checks if migrations require downtime or not' task :downtime_check, [:ref] => :environment do |_, args| abort 'You must specify a Git reference to compare with' unless args[:ref] @@ -71,7 +71,7 @@ namespace :gitlab do Gitlab::DowntimeCheck.new.check_and_print(migrations) end - desc 'Sets up EE specific database functionality' + desc 'GitLab | DB | Sets up EE specific database functionality' if Gitlab.ee? task setup_ee: %w[geo:db:drop geo:db:create geo:db:schema:load geo:db:migrate] diff --git a/lib/tasks/gitlab/exclusive_lease.rake b/lib/tasks/gitlab/exclusive_lease.rake index 83722bf6d94..63b06d5251a 100644 --- a/lib/tasks/gitlab/exclusive_lease.rake +++ b/lib/tasks/gitlab/exclusive_lease.rake @@ -1,6 +1,6 @@ namespace :gitlab do namespace :exclusive_lease do - desc 'GitLab | Clear existing exclusive leases for specified scope (default: *)' + desc 'GitLab | Exclusive Lease | Clear existing exclusive leases for specified scope (default: *)' task :clear, [:scope] => [:environment] do |_, args| args[:scope].nil? ? Gitlab::ExclusiveLease.reset_all! : Gitlab::ExclusiveLease.reset_all!(args[:scope]) puts 'All exclusive lease entries were removed.' diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 80de3d2ef51..c63ddb62f2a 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -1,6 +1,6 @@ namespace :gitlab do namespace :gitaly do - desc "GitLab | Install or upgrade gitaly" + desc 'GitLab | Gitaly | Install or upgrade gitaly' task :install, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args| warn_user_is_not_gitlab diff --git a/lib/tasks/gitlab/graphql.rake b/lib/tasks/gitlab/graphql.rake index f8ce3cd46a8..c73691f3d45 100644 --- a/lib/tasks/gitlab/graphql.rake +++ b/lib/tasks/gitlab/graphql.rake @@ -21,7 +21,7 @@ namespace :gitlab do ) namespace :graphql do - desc 'GitLab | Generate GraphQL docs' + desc 'GitLab | GraphQL | Generate GraphQL docs' task compile_docs: :environment do renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options) @@ -30,7 +30,7 @@ namespace :gitlab do puts "Documentation compiled." end - desc 'GitLab | Check if GraphQL docs are up to date' + desc 'GitLab | GraphQL | Check if GraphQL docs are up to date' task check_docs: :environment do renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options) @@ -44,7 +44,7 @@ namespace :gitlab do end end - desc 'GitLab | Check if GraphQL schemas are up to date' + desc 'GitLab | GraphQL | Check if GraphQL schemas are up to date' task check_schema: :environment do idl_doc = File.read(Rails.root.join(OUTPUT_DIR, 'gitlab_schema.graphql')) json_doc = File.read(Rails.root.join(OUTPUT_DIR, 'gitlab_schema.json')) diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index adfcc3cda22..701d40b7929 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -8,7 +8,7 @@ namespace :gitlab do # Notes: # * The project owner will set to the first administator of the system # * Existing projects will be skipped - desc "GitLab | Import bare repositories from repositories -> storages into GitLab project instance" + desc "GitLab | Import | Import bare repositories from repositories -> storages into GitLab project instance" task :repos, [:import_path] => :environment do |_t, args| unless args.import_path puts 'Please specify an import path that contains the repositories'.color(:red) diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake index 5365bd3920f..adf696350d7 100644 --- a/lib/tasks/gitlab/import_export.rake +++ b/lib/tasks/gitlab/import_export.rake @@ -1,16 +1,16 @@ namespace :gitlab do namespace :import_export do - desc "GitLab | Show Import/Export version" + desc 'GitLab | Import/Export | Show Import/Export version' task version: :environment do puts "Import/Export v#{Gitlab::ImportExport.version}" end - desc "GitLab | Display exported DB structure" + desc 'GitLab | Import/Export | Display exported DB structure' task data: :environment do puts Gitlab::ImportExport::Config.new.to_h['project_tree'].to_yaml(SortKeys: true) end - desc 'GitLab | Bumps the Import/Export version in fixtures and project templates' + desc 'GitLab | Import/Export | Bumps the Import/Export version in fixtures and project templates' task bump_version: :environment do archives = Dir['vendor/project_templates/*.tar.gz'] archives.push('spec/features/projects/import_export/test_project_export.tar.gz') diff --git a/lib/tasks/gitlab/import_export/import.rake b/lib/tasks/gitlab/import_export/import.rake index a88fb88c7ef..c832cba0287 100644 --- a/lib/tasks/gitlab/import_export/import.rake +++ b/lib/tasks/gitlab/import_export/import.rake @@ -7,12 +7,12 @@ # 2. Performs Sidekiq job synchronously # # @example -# bundle exec rake "gitlab:import_export:import[root, root, imported_project, /path/to/file.tar.gz]" +# bundle exec rake "gitlab:import_export:import[root, root, imported_project, /path/to/file.tar.gz, true]" # namespace :gitlab do namespace :import_export do - desc 'EXPERIMENTAL | Import large project archives' - task :import, [:username, :namespace_path, :project_path, :archive_path] => :gitlab_environment do |_t, args| + desc 'GitLab | Import/Export | EXPERIMENTAL | Import large project archives' + task :import, [:username, :namespace_path, :project_path, :archive_path, :measurement_enabled] => :gitlab_environment do |_t, args| # Load it here to avoid polluting Rake tasks with Sidekiq test warnings require 'sidekiq/testing' @@ -26,7 +26,8 @@ namespace :gitlab do namespace_path: args.namespace_path, project_path: args.project_path, username: args.username, - file_path: args.archive_path + file_path: args.archive_path, + measurement_enabled: args.measurement_enabled == 'true' ).import end end @@ -38,6 +39,7 @@ class GitlabProjectImport @file_path = opts.fetch(:file_path) @namespace = Namespace.find_by_full_path(opts.fetch(:namespace_path)) @current_user = User.find_by_username(opts.fetch(:username)) + @measurement_enabled = opts.fetch(:measurement_enabled) end def import @@ -72,21 +74,83 @@ class GitlabProjectImport RequestStore.clear! end + def with_count_queries(&block) + count = 0 + + counter_f = ->(name, started, finished, unique_id, payload) { + unless payload[:name].in? %w[CACHE SCHEMA] + count += 1 + end + } + + ActiveSupport::Notifications.subscribed(counter_f, "sql.active_record", &block) + + puts "Number of sql calls: #{count}" + end + + def with_gc_counter + gc_counts_before = GC.stat.select { |k, v| k =~ /count/ } + yield + gc_counts_after = GC.stat.select { |k, v| k =~ /count/ } + stats = gc_counts_before.merge(gc_counts_after) { |k, vb, va| va - vb } + puts "Total GC count: #{stats[:count]}" + puts "Minor GC count: #{stats[:minor_gc_count]}" + puts "Major GC count: #{stats[:major_gc_count]}" + end + + def with_measure_time + timing = Benchmark.realtime do + yield + end + + time = Time.at(timing).utc.strftime("%H:%M:%S") + puts "Time to finish: #{time}" + end + + def with_measuring + puts "Measuring enabled..." + with_gc_counter do + with_count_queries do + with_measure_time do + yield + end + end + end + end + + def measurement_enabled? + @measurement_enabled != false + end + # We want to ensure that all Sidekiq jobs are executed # synchronously as part of that process. # This ensures that all expensive operations do not escape # to general Sidekiq clusters/nodes. - def run_isolated_sidekiq_job + def with_isolated_sidekiq_job Sidekiq::Testing.fake! do with_request_store do - @project = create_project - - execute_sidekiq_job + # If you are attempting to import a large project into a development environment, + # you may see Gitaly throw an error about too many calls or invocations. + # This is due to a n+1 calls limit being set for development setups (not enforced in production) + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24475#note_283090635 + # For development setups, this code-path will be excluded from n+1 detection. + ::Gitlab::GitalyClient.allow_n_plus_1_calls do + measurement_enabled? ? with_measuring { yield } : yield + end end + true end end + def run_isolated_sidekiq_job + with_isolated_sidekiq_job do + @project = create_project + + execute_sidekiq_job + end + end + def create_project # We are disabling ObjectStorage for `import` # as it is too slow to handle big archives: diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index 8fadadccce9..5809f632c5a 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -1,6 +1,6 @@ namespace :gitlab do namespace :env do - desc "GitLab | Show information about GitLab and its environment" + desc 'GitLab | Env | Show information about GitLab and its environment' task info: :gitlab_environment do # check if there is an RVM environment rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s) diff --git a/lib/tasks/gitlab/lfs/migrate.rake b/lib/tasks/gitlab/lfs/migrate.rake index 6f11646c841..470a12c39cd 100644 --- a/lib/tasks/gitlab/lfs/migrate.rake +++ b/lib/tasks/gitlab/lfs/migrate.rake @@ -1,6 +1,6 @@ require 'logger' -desc "GitLab | Migrate LFS objects to remote storage" +desc "GitLab | LFS | Migrate LFS objects to remote storage" namespace :gitlab do namespace :lfs do task migrate: :environment do diff --git a/lib/tasks/gitlab/metrics.rake b/lib/tasks/gitlab/metrics.rake index 8a57e400dbe..f2635c96638 100644 --- a/lib/tasks/gitlab/metrics.rake +++ b/lib/tasks/gitlab/metrics.rake @@ -1,7 +1,7 @@ # frozen_string_literal: true namespace :metrics do - desc "GitLab | Setup common metrics" + desc "GitLab | Metrics | Setup common metrics" task setup_common_metrics: :gitlab_environment do ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute end diff --git a/lib/tasks/gitlab/seed/group_seed.rake b/lib/tasks/gitlab/seed/group_seed.rake new file mode 100644 index 00000000000..bc705c94422 --- /dev/null +++ b/lib/tasks/gitlab/seed/group_seed.rake @@ -0,0 +1,233 @@ +# frozen_string_literal: true + +# Seed test groups with: +# 1. 2 Subgroups per level +# 1. 2 Users & group members per group +# 1. 2 Epics, 2 Milestones & 2 Projects per group +# 1. Project issues +# +# It also assigns each project's issue with one of group's or ascendants +# groups milestone & epic. +# +# @param subgroups_depth - number of subgroup levels +# @param username - user creating subgroups (i.e. GitLab admin) +# +# @example +# bundle exec rake "gitlab:seed:group_seed[5, root]" +# +namespace :gitlab do + namespace :seed do + desc 'Seed groups with sub-groups/projects/epics/milestones for Group Import testing' + task :group_seed, [:subgroups_depth, :username] => :gitlab_environment do |_t, args| + require 'sidekiq/testing' + + GroupSeeder.new( + subgroups_depth: args.subgroups_depth, + username: args.username + ).seed + end + end +end + +class GroupSeeder + PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-test.git' + + attr_reader :all_group_ids + + def initialize(subgroups_depth:, username:) + @subgroups_depth = subgroups_depth.to_i + @user = User.find_by_username(username) + @group_names = Set.new + @resource_count = 2 + @all_groups = {} + @all_group_ids = [] + end + + def seed + create_groups + + puts 'Done!' + end + + def create_groups + create_root_group + create_sub_groups + create_users_and_members + create_epics if Gitlab.ee? + create_labels + create_milestones + + Sidekiq::Testing.inline! do + create_projects + end + end + + def create_users_and_members + all_group_ids.each do |group_id| + @resource_count.times do |_| + user = create_user + create_member(user.id, group_id) + end + end + end + + def create_root_group + root_group = ::Groups::CreateService.new(@user, group_params).execute + + track_group_id(1, root_group.id) + end + + def create_sub_groups + (2..@subgroups_depth).each do |level| + parent_level = level - 1 + current_level = level + parent_groups = @all_groups[parent_level] + + parent_groups.each do |parent_id| + @resource_count.times do |_| + sub_group = ::Groups::CreateService.new(@user, group_params(parent_id: parent_id)).execute + + track_group_id(current_level, sub_group.id) + end + end + end + end + + def track_group_id(depth_level, group_id) + @all_groups[depth_level] ||= [] + @all_groups[depth_level] << group_id + @all_group_ids << group_id + end + + def group_params(parent_id: nil) + name = unique_name + + { + name: name, + path: name, + parent_id: parent_id + } + end + + def unique_name + name = ffaker_name + name = ffaker_name until @group_names.add?(name) + name + end + + def ffaker_name + FFaker::Lorem.characters(5) + end + + def create_user + User.create!( + username: FFaker::Internet.user_name, + name: FFaker::Name.name, + email: FFaker::Internet.email, + confirmed_at: DateTime.now, + password: Devise.friendly_token + ) + end + + def create_member(user_id, group_id) + roles = Gitlab::Access.values + + GroupMember.create(user_id: user_id, access_level: roles.sample, source_id: group_id) + end + + def create_epics + all_group_ids.each do |group_id| + @resource_count.times do |_| + group = Group.find(group_id) + + epic_params = { + title: FFaker::Lorem.sentence(6), + description: FFaker::Lorem.paragraphs(3).join("\n\n"), + author: group.users.sample, + group: group + } + + Epic.create!(epic_params) + end + end + end + + def create_labels + all_group_ids.each do |group_id| + @resource_count.times do |_| + group = Group.find(group_id) + label_title = FFaker::Product.brand + + Labels::CreateService.new(title: label_title, color: "##{Digest::MD5.hexdigest(label_title)[0..5]}").execute(group: group) + end + end + end + + def create_milestones + all_group_ids.each do |group_id| + @resource_count.times do |i| + group = Group.find(group_id) + + milestone_params = { + title: "v#{i}.0", + description: FFaker::Lorem.sentence, + state: [:active, :closed].sample + } + + Milestones::CreateService.new(group, group.members.sample, milestone_params).execute + end + end + end + + def create_projects + all_group_ids.each do |group_id| + group = Group.find(group_id) + + @resource_count.times do |i| + _, project_path = PROJECT_URL.split('/')[-2..-1] + + project_path.gsub!('.git', '') + + params = { + import_url: PROJECT_URL, + namespace_id: group.id, + name: project_path.titleize + FFaker::Lorem.characters(10), + description: FFaker::Lorem.sentence, + visibility_level: 0, + skip_disk_validation: true + } + + project = nil + + Sidekiq::Worker.skipping_transaction_check do + project = ::Projects::CreateService.new(@user, params).execute + project.send(:_run_after_commit_queue) + project.import_state.send(:_run_after_commit_queue) + project.repository.expire_all_method_caches + end + + create_project_issues(project) + assign_issues_to_epics_and_milestones(project) + end + end + end + + def create_project_issues(project) + seeder = Quality::Seeders::Issues.new(project: project) + seeder.seed(backfill_weeks: 2, average_issues_per_week: 2) + end + + def assign_issues_to_epics_and_milestones(project) + group_ids = project.group.self_and_ancestors.map(&:id) + + project.issues.each do |issue| + issue_params = { + milestone: Milestone.where(group: group_ids).sample + } + + issue_params[:epic] = Epic.where(group: group_ids).sample if Gitlab.ee? + + issue.update(issue_params) + end + end +end diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index a592015963d..ba3e19caf3b 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -1,6 +1,6 @@ namespace :gitlab do namespace :shell do - desc "GitLab | Install or upgrade gitlab-shell" + desc "GitLab | Shell | Install or upgrade gitlab-shell" task :install, [:repo] => :gitlab_environment do |t, args| warn_user_is_not_gitlab @@ -54,12 +54,12 @@ namespace :gitlab do Gitlab::Shell.ensure_secret_token! end - desc "GitLab | Setup gitlab-shell" + desc "GitLab | Shell | Setup gitlab-shell" task setup: :gitlab_environment do setup end - desc "GitLab | Build missing projects" + desc "GitLab | Shell | Build missing projects" task build_missing_projects: :gitlab_environment do Project.find_each(batch_size: 1000) do |project| path_to_repo = project.repository.path_to_repo diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake new file mode 100644 index 00000000000..eb3de195626 --- /dev/null +++ b/lib/tasks/gitlab/sidekiq.rake @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +return if Rails.env.production? + +namespace :gitlab do + namespace :sidekiq do + def write_yaml(path, banner, object) + File.write(path, banner + YAML.dump(object)) + end + + namespace :all_queues_yml do + desc 'GitLab | Sidekiq | Generate all_queues.yml based on worker definitions' + task generate: :environment do + banner = <<~BANNER + # This file is generated automatically by + # bin/rake gitlab:sidekiq:all_queues_yml:generate + # + # Do not edit it manually! + BANNER + + foss_workers, ee_workers = Gitlab::SidekiqConfig.workers_for_all_queues_yml + + write_yaml(Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH, banner, foss_workers) + + if Gitlab.ee? + write_yaml(Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH, banner, ee_workers) + end + end + + desc 'GitLab | Sidekiq | Validate that all_queues.yml matches worker definitions' + task check: :environment do + if Gitlab::SidekiqConfig.all_queues_yml_outdated? + raise <<~MSG + Changes in worker queues found, please update the metadata by running: + + bin/rake gitlab:sidekiq:all_queues_yml:generate + + Then commit and push the changes from: + + - #{Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH} + - #{Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH} + + MSG + end + end + end + + namespace :sidekiq_queues_yml do + desc 'GitLab | Sidekiq | Generate sidekiq_queues.yml based on worker definitions' + task generate: :environment do + banner = <<~BANNER + # This file is generated automatically by + # bin/rake gitlab:sidekiq:sidekiq_queues_yml:generate + # + # Do not edit it manually! + # + # This configuration file should be exclusively used to set queue settings for + # Sidekiq. Any other setting should be specified using the Sidekiq CLI or the + # Sidekiq Ruby API (see config/initializers/sidekiq.rb). + # + # All the queues to process and their weights. Every queue _must_ have a weight + # defined. + # + # The available weights are as follows + # + # 1: low priority + # 2: medium priority + # 3: high priority + # 5: _super_ high priority, this should only be used for _very_ important queues + # + # As per http://stackoverflow.com/a/21241357/290102 the formula for calculating + # the likelihood of a job being popped off a queue (given all queues have work + # to perform) is: + # + # chance = (queue weight / total weight of all queues) * 100 + BANNER + + queues_and_weights = Gitlab::SidekiqConfig.queues_for_sidekiq_queues_yml + + write_yaml(Gitlab::SidekiqConfig::SIDEKIQ_QUEUES_PATH, banner, queues: queues_and_weights) + end + + desc 'GitLab | Sidekiq | Validate that sidekiq_queues.yml matches worker definitions' + task check: :environment do + if Gitlab::SidekiqConfig.sidekiq_queues_yml_outdated? + raise <<~MSG + Changes in worker queues found, please update the metadata by running: + + bin/rake gitlab:sidekiq:sidekiq_queues_yml:generate + + Then commit and push the changes from: + + - #{Gitlab::SidekiqConfig::SIDEKIQ_QUEUES_PATH} + + MSG + end + end + end + end +end diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake index 6b22499a5c8..6a9e87e1541 100644 --- a/lib/tasks/gitlab/two_factor.rake +++ b/lib/tasks/gitlab/two_factor.rake @@ -1,6 +1,6 @@ namespace :gitlab do namespace :two_factor do - desc "GitLab | Disable Two-factor authentication (2FA) for all users" + desc "GitLab | 2FA | Disable Two-factor authentication (2FA) for all users" task disable_for_all_users: :gitlab_environment do scope = User.with_two_factor count = scope.count @@ -25,12 +25,12 @@ namespace :gitlab do @rotator ||= Gitlab::OtpKeyRotator.new(ENV['filename']) end - desc "Encrypt user OTP secrets with a new encryption key" + desc "GitLab | 2FA | Rotate Key | Encrypt user OTP secrets with a new encryption key" task apply: :environment do |t, args| rotator.rotate!(old_key: ENV['old_key'], new_key: ENV['new_key']) end - desc "Rollback to secrets encrypted with the old encryption key" + desc "GitLab | 2FA | Rotate Key | Rollback to secrets encrypted with the old encryption key" task rollback: :environment do rotator.rollback! end diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake index 15cec80b6a6..0b98755a77c 100644 --- a/lib/tasks/gitlab/web_hook.rake +++ b/lib/tasks/gitlab/web_hook.rake @@ -1,6 +1,6 @@ namespace :gitlab do namespace :web_hook do - desc "GitLab | Adds a webhook to the projects" + desc "GitLab | Webhook | Adds a webhook to the projects" task add: :environment do web_hook_url = ENV['URL'] namespace_path = ENV['NAMESPACE'] @@ -20,7 +20,7 @@ namespace :gitlab do end end - desc "GitLab | Remove a webhook from the projects" + desc "GitLab | Webhook | Remove a webhook from the projects" task rm: :environment do web_hook_url = ENV['URL'] namespace_path = ENV['NAMESPACE'] @@ -44,7 +44,7 @@ namespace :gitlab do puts "#{count} webhooks were removed." end - desc "GitLab | List webhooks" + desc "GitLab | Webhook | List webhooks" task list: :environment do namespace_path = ENV['NAMESPACE'] diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake index b917a293095..bae3e4e8001 100644 --- a/lib/tasks/gitlab/workhorse.rake +++ b/lib/tasks/gitlab/workhorse.rake @@ -1,6 +1,6 @@ namespace :gitlab do namespace :workhorse do - desc "GitLab | Install or upgrade gitlab-workhorse" + desc "GitLab | Workhorse | Install or upgrade gitlab-workhorse" task :install, [:dir, :repo] => :gitlab_environment do |t, args| warn_user_is_not_gitlab diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake index f912f521dfb..500891df43d 100644 --- a/lib/tasks/import.rake +++ b/lib/tasks/import.rake @@ -138,7 +138,7 @@ class GithubRepos end namespace :import do - desc 'Import a GitHub project - Example: import:github[ToKeN,root,root/blah,my/github_repo] (optional my/github_repo)' + desc 'GitLab | Import | Import a GitHub project - Example: import:github[ToKeN,root,root/blah,my/github_repo] (optional my/github_repo)' task :github, [:token, :gitlab_username, :project_path] => :environment do |_t, args| abort 'Project path must be: namespace(s)/project_name'.color(:red) unless args.project_path.include?('/') diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake index 9a5693e78a2..7a4d09bb6d4 100644 --- a/lib/tasks/lint.rake +++ b/lib/tasks/lint.rake @@ -4,7 +4,7 @@ unless Rails.env.production? ENV['STATIC_VERIFICATION'] = 'true' end - desc "GitLab | lint | Static verification" + desc "GitLab | Lint | Static verification" task static_verification: %w[ lint:static_verification_env dev:load @@ -12,19 +12,19 @@ unless Rails.env.production? Gitlab::Utils::Override.verify! end - desc "GitLab | lint | Lint JavaScript files using ESLint" + desc "GitLab | Lint | Lint JavaScript files using ESLint" task :javascript do Rake::Task['eslint'].invoke end - desc "GitLab | lint | Lint HAML files" + desc "GitLab | Lint | Lint HAML files" task :haml do Rake::Task['haml_lint'].invoke rescue RuntimeError # The haml_lint tasks raise a RuntimeError exit(1) end - desc "GitLab | lint | Run several lint checks" + desc "GitLab | Lint | Run several lint checks" task :all do status = 0 @@ -34,14 +34,17 @@ unless Rails.env.production? scss_lint gettext:lint lint:static_verification + gitlab:sidekiq:all_queues_yml:check ] if Gitlab.ee? - # This task will fail on CE installations (e.g. gitlab-org/gitlab-foss) - # since it will detect strings in the locale files that do not exist in - # the source files. To work around this we will only enable this task on - # EE installations. + # These tasks will fail on FOSS installations + # (e.g. gitlab-org/gitlab-foss) since they test against a single + # file that is generated by an EE installation, which can + # contain values that a FOSS installation won't find. To work + # around this we will only enable this task on EE installations. tasks << 'gettext:updated_check' + tasks << 'gitlab:sidekiq:sidekiq_queues_yml:check' end tasks.each do |task| diff --git a/lib/tasks/migrate/composite_primary_keys.rake b/lib/tasks/migrate/composite_primary_keys.rake index eb112434dd9..732dedf4d4f 100644 --- a/lib/tasks/migrate/composite_primary_keys.rake +++ b/lib/tasks/migrate/composite_primary_keys.rake @@ -1,12 +1,12 @@ namespace :gitlab do namespace :db do - desc 'GitLab | Adds primary keys to tables that only have composite unique keys' + desc 'GitLab | DB | Adds primary keys to tables that only have composite unique keys' task composite_primary_keys_add: :environment do require Rails.root.join('db/optional_migrations/composite_primary_keys') CompositePrimaryKeysMigration.new.up end - desc 'GitLab | Removes previously added composite primary keys' + desc 'GitLab | DB | Removes previously added composite primary keys' task composite_primary_keys_drop: :environment do require Rails.root.join('db/optional_migrations/composite_primary_keys') CompositePrimaryKeysMigration.new.down diff --git a/lib/tasks/pngquant.rake b/lib/tasks/pngquant.rake index 56dfd5ed081..ceb4de55373 100644 --- a/lib/tasks/pngquant.rake +++ b/lib/tasks/pngquant.rake @@ -53,7 +53,7 @@ namespace :pngquant do end end - desc 'GitLab | pngquant | Compress all documentation PNG images using pngquant' + desc 'GitLab | Pngquant | Compress all documentation PNG images using pngquant' task :compress do check_executable @@ -69,7 +69,7 @@ namespace :pngquant do end end - desc 'GitLab | pngquant | Checks that all documentation PNG images have been compressed with pngquant' + desc 'GitLab | Pngquant | Checks that all documentation PNG images have been compressed with pngquant' task :lint do check_executable diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake index cb9f4c751ed..e281ebd5d60 100644 --- a/lib/tasks/sidekiq.rake +++ b/lib/tasks/sidekiq.rake @@ -8,28 +8,28 @@ namespace :sidekiq do WARNING end - desc "[DEPRECATED] GitLab | Stop sidekiq" + desc '[DEPRECATED] GitLab | Sidekiq | Stop sidekiq' task :stop do deprecation_warning! system(*%w(bin/background_jobs stop)) end - desc "[DEPRECATED] GitLab | Start sidekiq" + desc '[DEPRECATED] GitLab | Sidekiq | Start sidekiq' task :start do deprecation_warning! system(*%w(bin/background_jobs start)) end - desc '[DEPRECATED] GitLab | Restart sidekiq' + desc '[DEPRECATED] GitLab | Sidekiq | Restart sidekiq' task :restart do deprecation_warning! system(*%w(bin/background_jobs restart)) end - desc "[DEPRECATED] GitLab | Start sidekiq with launchd on Mac OS X" + desc '[DEPRECATED] GitLab | Sidekiq | Start sidekiq with launchd on Mac OS X' task :launchd do deprecation_warning! diff --git a/lib/tasks/yarn.rake b/lib/tasks/yarn.rake index 32061ad4a57..667d850d2de 100644 --- a/lib/tasks/yarn.rake +++ b/lib/tasks/yarn.rake @@ -1,3 +1,4 @@ +# frozen_string_literal: true namespace :yarn do desc 'Ensure Yarn is installed' |