From 411cc77938f99b495e0fe802705d275a28e939ef Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 24 Jan 2020 18:09:00 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- CHANGELOG.md | 4 - .../components/error_tracking_list.vue | 14 +- .../javascripts/jobs/components/sidebar.vue | 1 + .../projects/git_http_client_controller.rb | 122 ------- .../projects/git_http_controller.rb | 117 ------ .../projects/lfs_api_controller.rb | 140 -------- .../projects/lfs_locks_api_controller.rb | 76 ---- .../projects/lfs_storage_controller.rb | 89 ----- app/controllers/projects_controller.rb | 1 + .../repositories/application_controller.rb | 7 + .../git_http_client_controller.rb | 125 +++++++ .../repositories/git_http_controller.rb | 119 +++++++ .../repositories/lfs_api_controller.rb | 142 ++++++++ .../repositories/lfs_locks_api_controller.rb | 78 ++++ .../repositories/lfs_storage_controller.rb | 91 +++++ .../sentry_detailed_error_type.rb | 56 +-- .../error_tracking/sentry_error_tags_type.rb | 19 + app/helpers/blob_helper.rb | 4 +- app/models/clusters/applications/ingress.rb | 3 +- app/models/project.rb | 2 + app/views/projects/blob/_header.html.haml | 12 +- .../projects/blob/_viewer_switcher.html.haml | 2 +- app/views/shared/members/_member.html.haml | 2 + app/workers/authorized_projects_worker.rb | 1 + app/workers/chat_notification_worker.rb | 2 + .../self_monitoring_project_worker.rb | 1 + app/workers/concerns/worker_attributes.rb | 28 ++ app/workers/create_evidence_worker.rb | 1 + app/workers/create_gpg_signature_worker.rb | 1 + app/workers/email_receiver_worker.rb | 1 + app/workers/emails_on_push_worker.rb | 1 + app/workers/gitlab_shell_worker.rb | 1 + app/workers/import_issues_csv_worker.rb | 1 + .../invalid_gpg_signature_update_worker.rb | 1 + app/workers/merge_worker.rb | 1 + app/workers/new_issue_worker.rb | 1 + app/workers/new_merge_request_worker.rb | 1 + app/workers/new_note_worker.rb | 1 + app/workers/new_release_worker.rb | 1 + app/workers/post_receive.rb | 1 + app/workers/process_commit_worker.rb | 1 + app/workers/rebase_worker.rb | 1 + .../remote_mirror_notification_worker.rb | 1 + .../update_external_pull_requests_worker.rb | 1 + app/workers/update_merge_requests_worker.rb | 1 + .../14707-add-waf-anomaly-summary-service.yml | 5 + ...-missing-detailed-sentry-error-graphql.yml | 5 + ...-like-segmented-control-in-file-header.yml | 5 + ...-add-ci-variables-server-port-protocol.yml | 5 + changelogs/unreleased/broadcast-api-auth.yml | 5 + ...ssing-apply-sugegstion-project-setting.yml | 5 + .../integrate-gitaly-disk_statistics-grpc.yml | 5 + config/initializers/lograge.rb | 4 + config/routes/git_http.rb | 78 ++-- config/sidekiq_queues.yml | 332 ++++++++++++------ ...124053531_add_source_to_import_failures.rb | 9 + db/schema.rb | 3 +- doc/api/broadcast_messages.md | 6 +- .../graphql/reference/gitlab_schema.graphql | 30 ++ doc/api/graphql/reference/gitlab_schema.json | 91 +++++ doc/api/graphql/reference/index.md | 12 + doc/ci/variables/README.md | 6 + doc/ci/variables/predefined_variables.md | 2 + .../suggestion_code_block_editor_v12_8.png | Bin 0 -> 9917 bytes .../suggestion_code_block_output_v12_8.png | Bin 0 -> 29769 bytes doc/user/discussions/index.md | 10 + lib/api/broadcast_messages.rb | 9 +- lib/api/helpers/internal_helpers.rb | 14 +- lib/constraints/project_url_constrainer.rb | 2 +- lib/feature/gitaly.rb | 1 - lib/gitaly/server.rb | 32 +- lib/gitlab/gitaly_client.rb | 13 +- lib/gitlab/gitaly_client/server_service.rb | 18 + lib/gitlab/gon_helper.rb | 3 + .../import_export/import_failure_service.rb | 12 +- .../import_export/project_tree_restorer.rb | 8 +- .../import_export/relation_tree_restorer.rb | 8 +- lib/gitlab/middleware/read_only/controller.rb | 6 +- lib/gitlab/sidekiq_config.rb | 39 +- lib/gitlab/sidekiq_config/worker.rb | 15 +- lib/tasks/gitlab/sidekiq.rake | 68 +++- lib/tasks/lint.rake | 10 +- locale/gitlab.pot | 3 + qa/qa/page/project/job/show.rb | 8 + qa/qa/page/project/settings/ci_variables.rb | 8 + .../deploy_key/clone_using_deploy_key_spec.rb | 18 +- spec/controllers/concerns/lfs_request_spec.rb | 2 +- .../projects/git_http_controller_spec.rb | 107 ------ .../repositories/git_http_controller_spec.rb | 146 ++++++++ .../sentry_detailed_error_type_spec.rb | 3 + spec/lib/gitaly/server_spec.rb | 47 +++ spec/lib/gitlab/gitaly_client_spec.rb | 59 +++- .../import_failure_service_spec.rb | 23 +- .../project_tree_restorer_spec.rb | 52 +++ spec/lib/gitlab/sidekiq_config/worker_spec.rb | 45 ++- spec/lib/gitlab/sidekiq_config_spec.rb | 60 ++++ spec/models/ci/build_spec.rb | 2 + spec/requests/api/broadcast_messages_spec.rb | 30 +- .../sentry_detailed_error_request_spec.rb | 4 + spec/requests/api/internal/base_spec.rb | 6 +- spec/requests/git_http_spec.rb | 16 +- .../import_failure_service_shared_examples.rb | 9 +- 102 files changed, 1842 insertions(+), 958 deletions(-) delete mode 100644 app/controllers/projects/git_http_client_controller.rb delete mode 100644 app/controllers/projects/git_http_controller.rb delete mode 100644 app/controllers/projects/lfs_api_controller.rb delete mode 100644 app/controllers/projects/lfs_locks_api_controller.rb delete mode 100644 app/controllers/projects/lfs_storage_controller.rb create mode 100644 app/controllers/repositories/application_controller.rb create mode 100644 app/controllers/repositories/git_http_client_controller.rb create mode 100644 app/controllers/repositories/git_http_controller.rb create mode 100644 app/controllers/repositories/lfs_api_controller.rb create mode 100644 app/controllers/repositories/lfs_locks_api_controller.rb create mode 100644 app/controllers/repositories/lfs_storage_controller.rb create mode 100644 app/graphql/types/error_tracking/sentry_error_tags_type.rb create mode 100644 changelogs/unreleased/14707-add-waf-anomaly-summary-service.yml create mode 100644 changelogs/unreleased/194164-add-missing-detailed-sentry-error-graphql.yml create mode 100644 changelogs/unreleased/197500-edit-and-web-ide-look-like-segmented-control-in-file-header.yml create mode 100644 changelogs/unreleased/23296-add-ci-variables-server-port-protocol.yml create mode 100644 changelogs/unreleased/broadcast-api-auth.yml create mode 100644 changelogs/unreleased/fix-missing-apply-sugegstion-project-setting.yml create mode 100644 changelogs/unreleased/integrate-gitaly-disk_statistics-grpc.yml create mode 100644 db/migrate/20200124053531_add_source_to_import_failures.rb create mode 100644 doc/user/discussions/img/suggestion_code_block_editor_v12_8.png create mode 100644 doc/user/discussions/img/suggestion_code_block_output_v12_8.png delete mode 100644 spec/controllers/projects/git_http_controller_spec.rb create mode 100644 spec/controllers/repositories/git_http_controller_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a1552ffb2d..a147bd438b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,6 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. -## 12.7.2 - -- No changes. - ## 12.7.1 ### Fixed (6 changes) diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index 3280ff48129..e57e17c38c1 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -20,6 +20,8 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { __ } from '~/locale'; import _ from 'underscore'; +export const tableDataClass = 'table-col d-flex d-sm-table-cell'; + export default { FIRST_PAGE: 1, PREV_PAGE: 1, @@ -29,37 +31,37 @@ export default { key: 'error', label: __('Error'), thClass: 'w-60p', - tdClass: 'table-col d-flex d-sm-table-cell px-3', + tdClass: `${tableDataClass} px-3`, }, { key: 'events', label: __('Events'), thClass: 'text-right', - tdClass: 'table-col d-flex d-sm-table-cell', + tdClass: `${tableDataClass}`, }, { key: 'users', label: __('Users'), thClass: 'text-right', - tdClass: 'table-col d-flex d-sm-table-cell', + tdClass: `${tableDataClass}`, }, { key: 'lastSeen', label: __('Last seen'), thClass: '', - tdClass: 'table-col d-flex d-sm-table-cell', + tdClass: `${tableDataClass}`, }, { key: 'ignore', label: '', thClass: 'w-3rem', - tdClass: 'table-col d-flex pl-0 d-sm-table-cell', + tdClass: `${tableDataClass} pl-0`, }, { key: 'resolved', label: '', thClass: 'w-3rem', - tdClass: 'table-col d-flex pl-0 d-sm-table-cell', + tdClass: `${tableDataClass} pl-0`, }, { key: 'details', diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index 415fa46835b..a61acf2f6eb 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -119,6 +119,7 @@ export default { :class="retryButtonClass" :href="job.retry_path" data-method="post" + data-qa-selector="retry_button" rel="nofollow" >{{ __('Retry') }} diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb deleted file mode 100644 index 3f6e116a62b..00000000000 --- a/app/controllers/projects/git_http_client_controller.rb +++ /dev/null @@ -1,122 +0,0 @@ -# frozen_string_literal: true - -class Projects::GitHttpClientController < Projects::ApplicationController - include ActionController::HttpAuthentication::Basic - include KerberosSpnegoHelper - include Gitlab::Utils::StrongMemoize - - attr_reader :authentication_result, :redirected_path - - delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true - delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result - - alias_method :user, :actor - alias_method :authenticated_user, :actor - - # Git clients will not know what authenticity token to send along - skip_around_action :set_session_storage - skip_before_action :verify_authenticity_token - skip_before_action :repository - before_action :authenticate_user - - private - - def download_request? - raise NotImplementedError - end - - def upload_request? - raise NotImplementedError - end - - def authenticate_user - @authentication_result = Gitlab::Auth::Result.new - - if allow_basic_auth? && basic_auth_provided? - login, password = user_name_and_password(request) - - if handle_basic_authentication(login, password) - return # Allow access - end - elsif allow_kerberos_spnego_auth? && spnego_provided? - kerberos_user = find_kerberos_user - - if kerberos_user - @authentication_result = Gitlab::Auth::Result.new( - kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities) - - send_final_spnego_response - return # Allow access - end - elsif http_download_allowed? - - @authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code]) - - return # Allow access - end - - send_challenges - render plain: "HTTP Basic: Access denied\n", status: :unauthorized - rescue Gitlab::Auth::MissingPersonalAccessTokenError - render_missing_personal_access_token - end - - def basic_auth_provided? - has_basic_credentials?(request) - end - - def send_challenges - challenges = [] - challenges << 'Basic realm="GitLab"' if allow_basic_auth? - challenges << spnego_challenge if allow_kerberos_spnego_auth? - headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? - end - - def project - parse_repo_path unless defined?(@project) - - @project - end - - def parse_repo_path - @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}") - end - - def render_missing_personal_access_token - render plain: "HTTP Basic: Access denied\n" \ - "You must use a personal access token with 'read_repository' or 'write_repository' scope for Git over HTTP.\n" \ - "You can generate one at #{profile_personal_access_tokens_url}", - status: :unauthorized - end - - def repository - strong_memoize(:repository) do - repo_type.repository_for(project) - end - end - - def repo_type - parse_repo_path unless defined?(@repo_type) - - @repo_type - end - - def handle_basic_authentication(login, password) - @authentication_result = Gitlab::Auth.find_for_git_client( - login, password, project: project, ip: request.ip) - - @authentication_result.success? - end - - def ci? - authentication_result.ci?(project) - end - - def http_download_allowed? - Gitlab::ProtocolAccess.allowed?('http') && - download_request? && - project && Guest.can?(:download_code, project) - end -end - -Projects::GitHttpClientController.prepend_if_ee('EE::Projects::GitHttpClientController') diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb deleted file mode 100644 index 236f1b967de..00000000000 --- a/app/controllers/projects/git_http_controller.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true - -class Projects::GitHttpController < Projects::GitHttpClientController - include WorkhorseRequest - - before_action :access_check - prepend_before_action :deny_head_requests, only: [:info_refs] - - rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403_with_exception - rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception - rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422_with_exception - rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception - - # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) - # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) - def info_refs - log_user_activity if upload_pack? - - render_ok - end - - # POST /foo/bar.git/git-upload-pack (git pull) - def git_upload_pack - enqueue_fetch_statistics_update - - render_ok - end - - # POST /foo/bar.git/git-receive-pack" (git push) - def git_receive_pack - render_ok - end - - private - - def deny_head_requests - head :forbidden if request.head? - end - - def download_request? - upload_pack? - end - - def upload_pack? - git_command == 'git-upload-pack' - end - - def git_command - if action_name == 'info_refs' - params[:service] - else - action_name.dasherize - end - end - - def render_ok - set_workhorse_internal_api_content_type - render json: Gitlab::Workhorse.git_http_ok(repository, repo_type, user, action_name) - end - - def render_403_with_exception(exception) - render plain: exception.message, status: :forbidden - end - - def render_404_with_exception(exception) - render plain: exception.message, status: :not_found - end - - def render_422_with_exception(exception) - render plain: exception.message, status: :unprocessable_entity - end - - def render_503_with_exception(exception) - render plain: exception.message, status: :service_unavailable - end - - def enqueue_fetch_statistics_update - return if Gitlab::Database.read_only? - return if repo_type.wiki? - return unless project&.daily_statistics_enabled? - - ProjectDailyStatisticsWorker.perform_async(project.id) - end - - def access - @access ||= access_klass.new(access_actor, project, 'http', - authentication_abilities: authentication_abilities, - namespace_path: params[:namespace_id], - project_path: project_path, - redirected_path: redirected_path, - auth_result_type: auth_result_type) - end - - def access_actor - return user if user - return :ci if ci? - end - - def access_check - access.check(git_command, Gitlab::GitAccess::ANY) - @project ||= access.project - end - - def access_klass - @access_klass ||= repo_type.access_checker_class - end - - def project_path - @project_path ||= params[:project_id].sub(/\.git$/, '') - end - - def log_user_activity - Users::ActivityService.new(user).execute - end -end - -Projects::GitHttpController.prepend_if_ee('EE::Projects::GitHttpController') diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb deleted file mode 100644 index 1273c55b83a..00000000000 --- a/app/controllers/projects/lfs_api_controller.rb +++ /dev/null @@ -1,140 +0,0 @@ -# frozen_string_literal: true - -class Projects::LfsApiController < Projects::GitHttpClientController - include LfsRequest - include Gitlab::Utils::StrongMemoize - - LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream' - - skip_before_action :lfs_check_access!, only: [:deprecated] - before_action :lfs_check_batch_operation!, only: [:batch] - - def batch - unless objects.present? - render_lfs_not_found - return - end - - if download_request? - render json: { objects: download_objects! } - elsif upload_request? - render json: { objects: upload_objects! } - else - raise "Never reached" - end - end - - def deprecated - render( - json: { - message: _('Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.'), - documentation_url: "#{Gitlab.config.gitlab.url}/help" - }, - status: :not_implemented - ) - end - - private - - def download_request? - params[:operation] == 'download' - end - - def upload_request? - params[:operation] == 'upload' - end - - # rubocop: disable CodeReuse/ActiveRecord - def existing_oids - @existing_oids ||= begin - project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid) - end - end - # rubocop: enable CodeReuse/ActiveRecord - - def download_objects! - objects.each do |object| - if existing_oids.include?(object[:oid]) - object[:actions] = download_actions(object) - - if Guest.can?(:download_code, project) - object[:authenticated] = true - end - else - object[:error] = { - code: 404, - message: _("Object does not exist on the server or you don't have permissions to access it") - } - end - end - objects - end - - def upload_objects! - objects.each do |object| - object[:actions] = upload_actions(object) unless existing_oids.include?(object[:oid]) - end - objects - end - - def download_actions(object) - { - download: { - href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}", - header: { - Authorization: authorization_header - }.compact - } - } - end - - def upload_actions(object) - { - upload: { - href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}", - header: { - Authorization: authorization_header, - # git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This - # ensures that Workhorse can intercept the request. - 'Content-Type': LFS_TRANSFER_CONTENT_TYPE - }.compact - } - } - end - - def lfs_check_batch_operation! - if batch_operation_disallowed? - render( - json: { - message: lfs_read_only_message - }, - content_type: LfsRequest::CONTENT_TYPE, - status: :forbidden - ) - end - end - - # Overridden in EE - def batch_operation_disallowed? - upload_request? && Gitlab::Database.read_only? - end - - # Overridden in EE - def lfs_read_only_message - _('You cannot write to this read-only GitLab instance.') - end - - def authorization_header - strong_memoize(:authorization_header) do - lfs_auth_header || request.headers['Authorization'] - end - end - - def lfs_auth_header - return unless user.is_a?(User) - - Gitlab::LfsToken.new(user).basic_encoding - end -end - -Projects::LfsApiController.prepend_if_ee('EE::Projects::LfsApiController') diff --git a/app/controllers/projects/lfs_locks_api_controller.rb b/app/controllers/projects/lfs_locks_api_controller.rb deleted file mode 100644 index 6aacb9d9a56..00000000000 --- a/app/controllers/projects/lfs_locks_api_controller.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -class Projects::LfsLocksApiController < Projects::GitHttpClientController - include LfsRequest - - def create - @result = Lfs::LockFileService.new(project, user, lfs_params).execute - - render_json(@result[:lock]) - end - - def unlock - @result = Lfs::UnlockFileService.new(project, user, lfs_params).execute - - render_json(@result[:lock]) - end - - def index - @result = Lfs::LocksFinderService.new(project, user, lfs_params).execute - - render_json(@result[:locks]) - end - - def verify - @result = Lfs::LocksFinderService.new(project, user, {}).execute - - ours, theirs = split_by_owner(@result[:locks]) - - render_json({ ours: ours, theirs: theirs }, false) - end - - private - - def render_json(data, process = true) - render json: build_payload(data, process), - content_type: LfsRequest::CONTENT_TYPE, - status: @result[:http_status] - end - - def build_payload(data, process) - data = LfsFileLockSerializer.new.represent(data) if process - - return data if @result[:status] == :success - - # When the locking failed due to an existent Lock, the existent record - # is returned in `@result[:lock]` - error_payload(@result[:message], @result[:lock] ? data : {}) - end - - def error_payload(message, custom_attrs = {}) - custom_attrs.merge({ - message: message, - documentation_url: help_url - }) - end - - def split_by_owner(locks) - groups = locks.partition { |lock| lock.user_id == user.id } - - groups.map! do |records| - LfsFileLockSerializer.new.represent(records, root: false) - end - end - - def download_request? - params[:action] == 'index' - end - - def upload_request? - %w(create unlock verify).include?(params[:action]) - end - - def lfs_params - params.permit(:id, :path, :force) - end -end diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb deleted file mode 100644 index 013e01b82aa..00000000000 --- a/app/controllers/projects/lfs_storage_controller.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -class Projects::LfsStorageController < Projects::GitHttpClientController - include LfsRequest - include WorkhorseRequest - include SendFileUpload - - skip_before_action :verify_workhorse_api!, only: :download - - def download - lfs_object = LfsObject.find_by_oid(oid) - unless lfs_object && lfs_object.file.exists? - render_lfs_not_found - return - end - - send_upload(lfs_object.file, send_params: { content_type: "application/octet-stream" }) - end - - def upload_authorize - set_workhorse_internal_api_content_type - - authorized = LfsObjectUploader.workhorse_authorize(has_length: true) - authorized.merge!(LfsOid: oid, LfsSize: size) - - render json: authorized - end - - def upload_finalize - if store_file!(oid, size) - head 200 - else - render plain: 'Unprocessable entity', status: :unprocessable_entity - end - rescue ActiveRecord::RecordInvalid - render_lfs_forbidden - rescue UploadedFile::InvalidPathError - render_lfs_forbidden - rescue ObjectStorage::RemoteStoreError - render_lfs_forbidden - end - - private - - def download_request? - action_name == 'download' - end - - def upload_request? - %w[upload_authorize upload_finalize].include? action_name - end - - def oid - params[:oid].to_s - end - - def size - params[:size].to_i - end - - # rubocop: disable CodeReuse/ActiveRecord - def store_file!(oid, size) - object = LfsObject.find_by(oid: oid, size: size) - unless object&.file&.exists? - object = create_file!(oid, size) - end - - return unless object - - link_to_project!(object) - end - # rubocop: enable CodeReuse/ActiveRecord - - def create_file!(oid, size) - uploaded_file = UploadedFile.from_params( - params, :file, LfsObjectUploader.workhorse_local_upload_path) - return unless uploaded_file - - LfsObject.create!(oid: oid, size: size, file: uploaded_file) - end - - # rubocop: disable CodeReuse/ActiveRecord - def link_to_project!(object) - if object && !object.projects.exists?(storage_project.id) - object.lfs_objects_projects.create!(project: storage_project) - end - end - # rubocop: enable CodeReuse/ActiveRecord -end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index bf05defbc2e..f4f2a16b82b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -387,6 +387,7 @@ class ProjectsController < Projects::ApplicationController :merge_method, :initialize_with_readme, :autoclose_referenced_issues, + :suggestion_commit_message, project_feature_attributes: %i[ builds_access_level diff --git a/app/controllers/repositories/application_controller.rb b/app/controllers/repositories/application_controller.rb new file mode 100644 index 00000000000..528cc310038 --- /dev/null +++ b/app/controllers/repositories/application_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Repositories + class ApplicationController < ::ApplicationController + skip_before_action :authenticate_user! + end +end diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb new file mode 100644 index 00000000000..76eb7c67205 --- /dev/null +++ b/app/controllers/repositories/git_http_client_controller.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +module Repositories + class GitHttpClientController < Repositories::ApplicationController + include ActionController::HttpAuthentication::Basic + include KerberosSpnegoHelper + include Gitlab::Utils::StrongMemoize + + attr_reader :authentication_result, :redirected_path + + delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true + delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result + + alias_method :user, :actor + alias_method :authenticated_user, :actor + + # Git clients will not know what authenticity token to send along + skip_around_action :set_session_storage + skip_before_action :verify_authenticity_token + + before_action :parse_repo_path + before_action :authenticate_user + + private + + def download_request? + raise NotImplementedError + end + + def upload_request? + raise NotImplementedError + end + + def authenticate_user + @authentication_result = Gitlab::Auth::Result.new + + if allow_basic_auth? && basic_auth_provided? + login, password = user_name_and_password(request) + + if handle_basic_authentication(login, password) + return # Allow access + end + elsif allow_kerberos_spnego_auth? && spnego_provided? + kerberos_user = find_kerberos_user + + if kerberos_user + @authentication_result = Gitlab::Auth::Result.new( + kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities) + + send_final_spnego_response + return # Allow access + end + elsif http_download_allowed? + + @authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code]) + + return # Allow access + end + + send_challenges + render plain: "HTTP Basic: Access denied\n", status: :unauthorized + rescue Gitlab::Auth::MissingPersonalAccessTokenError + render_missing_personal_access_token + end + + def basic_auth_provided? + has_basic_credentials?(request) + end + + def send_challenges + challenges = [] + challenges << 'Basic realm="GitLab"' if allow_basic_auth? + challenges << spnego_challenge if allow_kerberos_spnego_auth? + headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? + end + + def project + parse_repo_path unless defined?(@project) + + @project + end + + def parse_repo_path + @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:repository_id]}") + end + + def render_missing_personal_access_token + render plain: "HTTP Basic: Access denied\n" \ + "You must use a personal access token with 'read_repository' or 'write_repository' scope for Git over HTTP.\n" \ + "You can generate one at #{profile_personal_access_tokens_url}", + status: :unauthorized + end + + def repository + strong_memoize(:repository) do + repo_type.repository_for(project) + end + end + + def repo_type + parse_repo_path unless defined?(@repo_type) + + @repo_type + end + + def handle_basic_authentication(login, password) + @authentication_result = Gitlab::Auth.find_for_git_client( + login, password, project: project, ip: request.ip) + + @authentication_result.success? + end + + def ci? + authentication_result.ci?(project) + end + + def http_download_allowed? + Gitlab::ProtocolAccess.allowed?('http') && + download_request? && + project && Guest.can?(:download_code, project) + end + end +end + +Repositories::GitHttpClientController.prepend_if_ee('EE::Repositories::GitHttpClientController') diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb new file mode 100644 index 00000000000..82431ae286a --- /dev/null +++ b/app/controllers/repositories/git_http_controller.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +module Repositories + class GitHttpController < Repositories::GitHttpClientController + include WorkhorseRequest + + before_action :access_check + prepend_before_action :deny_head_requests, only: [:info_refs] + + rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403_with_exception + rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception + rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422_with_exception + rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception + + # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) + # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) + def info_refs + log_user_activity if upload_pack? + + render_ok + end + + # POST /foo/bar.git/git-upload-pack (git pull) + def git_upload_pack + enqueue_fetch_statistics_update + + render_ok + end + + # POST /foo/bar.git/git-receive-pack" (git push) + def git_receive_pack + render_ok + end + + private + + def deny_head_requests + head :forbidden if request.head? + end + + def download_request? + upload_pack? + end + + def upload_pack? + git_command == 'git-upload-pack' + end + + def git_command + if action_name == 'info_refs' + params[:service] + else + action_name.dasherize + end + end + + def render_ok + set_workhorse_internal_api_content_type + render json: Gitlab::Workhorse.git_http_ok(repository, repo_type, user, action_name) + end + + def render_403_with_exception(exception) + render plain: exception.message, status: :forbidden + end + + def render_404_with_exception(exception) + render plain: exception.message, status: :not_found + end + + def render_422_with_exception(exception) + render plain: exception.message, status: :unprocessable_entity + end + + def render_503_with_exception(exception) + render plain: exception.message, status: :service_unavailable + end + + def enqueue_fetch_statistics_update + return if Gitlab::Database.read_only? + return unless repo_type.project? + return unless project&.daily_statistics_enabled? + + ProjectDailyStatisticsWorker.perform_async(project.id) + end + + def access + @access ||= access_klass.new(access_actor, project, 'http', + authentication_abilities: authentication_abilities, + namespace_path: params[:namespace_id], + project_path: project_path, + redirected_path: redirected_path, + auth_result_type: auth_result_type) + end + + def access_actor + return user if user + return :ci if ci? + end + + def access_check + access.check(git_command, Gitlab::GitAccess::ANY) + @project ||= access.project + end + + def access_klass + @access_klass ||= repo_type.access_checker_class + end + + def project_path + @project_path ||= params[:repository_id].sub(/\.git$/, '') + end + + def log_user_activity + Users::ActivityService.new(user).execute + end + end +end + +Repositories::GitHttpController.prepend_if_ee('EE::Repositories::GitHttpController') diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb new file mode 100644 index 00000000000..b1e0d1848d7 --- /dev/null +++ b/app/controllers/repositories/lfs_api_controller.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +module Repositories + class LfsApiController < Repositories::GitHttpClientController + include LfsRequest + include Gitlab::Utils::StrongMemoize + + LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream' + + skip_before_action :lfs_check_access!, only: [:deprecated] + before_action :lfs_check_batch_operation!, only: [:batch] + + def batch + unless objects.present? + render_lfs_not_found + return + end + + if download_request? + render json: { objects: download_objects! } + elsif upload_request? + render json: { objects: upload_objects! } + else + raise "Never reached" + end + end + + def deprecated + render( + json: { + message: _('Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.'), + documentation_url: "#{Gitlab.config.gitlab.url}/help" + }, + status: :not_implemented + ) + end + + private + + def download_request? + params[:operation] == 'download' + end + + def upload_request? + params[:operation] == 'upload' + end + + # rubocop: disable CodeReuse/ActiveRecord + def existing_oids + @existing_oids ||= begin + project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def download_objects! + objects.each do |object| + if existing_oids.include?(object[:oid]) + object[:actions] = download_actions(object) + + if Guest.can?(:download_code, project) + object[:authenticated] = true + end + else + object[:error] = { + code: 404, + message: _("Object does not exist on the server or you don't have permissions to access it") + } + end + end + objects + end + + def upload_objects! + objects.each do |object| + object[:actions] = upload_actions(object) unless existing_oids.include?(object[:oid]) + end + objects + end + + def download_actions(object) + { + download: { + href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}", + header: { + Authorization: authorization_header + }.compact + } + } + end + + def upload_actions(object) + { + upload: { + href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}", + header: { + Authorization: authorization_header, + # git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This + # ensures that Workhorse can intercept the request. + 'Content-Type': LFS_TRANSFER_CONTENT_TYPE + }.compact + } + } + end + + def lfs_check_batch_operation! + if batch_operation_disallowed? + render( + json: { + message: lfs_read_only_message + }, + content_type: LfsRequest::CONTENT_TYPE, + status: :forbidden + ) + end + end + + # Overridden in EE + def batch_operation_disallowed? + upload_request? && Gitlab::Database.read_only? + end + + # Overridden in EE + def lfs_read_only_message + _('You cannot write to this read-only GitLab instance.') + end + + def authorization_header + strong_memoize(:authorization_header) do + lfs_auth_header || request.headers['Authorization'] + end + end + + def lfs_auth_header + return unless user.is_a?(User) + + Gitlab::LfsToken.new(user).basic_encoding + end + end +end + +Repositories::LfsApiController.prepend_if_ee('EE::Repositories::LfsApiController') diff --git a/app/controllers/repositories/lfs_locks_api_controller.rb b/app/controllers/repositories/lfs_locks_api_controller.rb new file mode 100644 index 00000000000..19fc09ad4de --- /dev/null +++ b/app/controllers/repositories/lfs_locks_api_controller.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Repositories + class LfsLocksApiController < Repositories::GitHttpClientController + include LfsRequest + + def create + @result = Lfs::LockFileService.new(project, user, lfs_params).execute + + render_json(@result[:lock]) + end + + def unlock + @result = Lfs::UnlockFileService.new(project, user, lfs_params).execute + + render_json(@result[:lock]) + end + + def index + @result = Lfs::LocksFinderService.new(project, user, lfs_params).execute + + render_json(@result[:locks]) + end + + def verify + @result = Lfs::LocksFinderService.new(project, user, {}).execute + + ours, theirs = split_by_owner(@result[:locks]) + + render_json({ ours: ours, theirs: theirs }, false) + end + + private + + def render_json(data, process = true) + render json: build_payload(data, process), + content_type: LfsRequest::CONTENT_TYPE, + status: @result[:http_status] + end + + def build_payload(data, process) + data = LfsFileLockSerializer.new.represent(data) if process + + return data if @result[:status] == :success + + # When the locking failed due to an existent Lock, the existent record + # is returned in `@result[:lock]` + error_payload(@result[:message], @result[:lock] ? data : {}) + end + + def error_payload(message, custom_attrs = {}) + custom_attrs.merge({ + message: message, + documentation_url: help_url + }) + end + + def split_by_owner(locks) + groups = locks.partition { |lock| lock.user_id == user.id } + + groups.map! do |records| + LfsFileLockSerializer.new.represent(records, root: false) + end + end + + def download_request? + params[:action] == 'index' + end + + def upload_request? + %w(create unlock verify).include?(params[:action]) + end + + def lfs_params + params.permit(:id, :path, :force) + end + end +end diff --git a/app/controllers/repositories/lfs_storage_controller.rb b/app/controllers/repositories/lfs_storage_controller.rb new file mode 100644 index 00000000000..58f496e16d3 --- /dev/null +++ b/app/controllers/repositories/lfs_storage_controller.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module Repositories + class LfsStorageController < Repositories::GitHttpClientController + include LfsRequest + include WorkhorseRequest + include SendFileUpload + + skip_before_action :verify_workhorse_api!, only: :download + + def download + lfs_object = LfsObject.find_by_oid(oid) + unless lfs_object && lfs_object.file.exists? + render_lfs_not_found + return + end + + send_upload(lfs_object.file, send_params: { content_type: "application/octet-stream" }) + end + + def upload_authorize + set_workhorse_internal_api_content_type + + authorized = LfsObjectUploader.workhorse_authorize(has_length: true) + authorized.merge!(LfsOid: oid, LfsSize: size) + + render json: authorized + end + + def upload_finalize + if store_file!(oid, size) + head 200 + else + render plain: 'Unprocessable entity', status: :unprocessable_entity + end + rescue ActiveRecord::RecordInvalid + render_lfs_forbidden + rescue UploadedFile::InvalidPathError + render_lfs_forbidden + rescue ObjectStorage::RemoteStoreError + render_lfs_forbidden + end + + private + + def download_request? + action_name == 'download' + end + + def upload_request? + %w[upload_authorize upload_finalize].include? action_name + end + + def oid + params[:oid].to_s + end + + def size + params[:size].to_i + end + + # rubocop: disable CodeReuse/ActiveRecord + def store_file!(oid, size) + object = LfsObject.find_by(oid: oid, size: size) + unless object&.file&.exists? + object = create_file!(oid, size) + end + + return unless object + + link_to_project!(object) + end + # rubocop: enable CodeReuse/ActiveRecord + + def create_file!(oid, size) + uploaded_file = UploadedFile.from_params( + params, :file, LfsObjectUploader.workhorse_local_upload_path) + return unless uploaded_file + + LfsObject.create!(oid: oid, size: size, file: uploaded_file) + end + + # rubocop: disable CodeReuse/ActiveRecord + def link_to_project!(object) + if object && !object.projects.exists?(storage_project.id) + object.lfs_objects_projects.create!(project: storage_project) + end + end + # rubocop: enable CodeReuse/ActiveRecord + end +end diff --git a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb index af6d8818d90..e3ccf9e61c8 100644 --- a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb +++ b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb @@ -11,77 +11,87 @@ module Types field :id, GraphQL::ID_TYPE, null: false, - description: "ID (global ID) of the error" + description: 'ID (global ID) of the error' field :sentry_id, GraphQL::STRING_TYPE, method: :id, null: false, - description: "ID (Sentry ID) of the error" + description: 'ID (Sentry ID) of the error' field :title, GraphQL::STRING_TYPE, null: false, - description: "Title of the error" + description: 'Title of the error' field :type, GraphQL::STRING_TYPE, null: false, - description: "Type of the error" + description: 'Type of the error' field :user_count, GraphQL::INT_TYPE, null: false, - description: "Count of users affected by the error" + description: 'Count of users affected by the error' field :count, GraphQL::INT_TYPE, null: false, - description: "Count of occurrences" + description: 'Count of occurrences' field :first_seen, Types::TimeType, null: false, - description: "Timestamp when the error was first seen" + description: 'Timestamp when the error was first seen' field :last_seen, Types::TimeType, null: false, - description: "Timestamp when the error was last seen" + description: 'Timestamp when the error was last seen' field :message, GraphQL::STRING_TYPE, null: true, - description: "Sentry metadata message of the error" + description: 'Sentry metadata message of the error' field :culprit, GraphQL::STRING_TYPE, null: false, - description: "Culprit of the error" + description: 'Culprit of the error' + field :external_base_url, GraphQL::STRING_TYPE, + null: false, + description: 'External Base URL of the Sentry Instance' field :external_url, GraphQL::STRING_TYPE, null: false, - description: "External URL of the error" + description: 'External URL of the error' field :sentry_project_id, GraphQL::ID_TYPE, method: :project_id, null: false, - description: "ID of the project (Sentry project)" + description: 'ID of the project (Sentry project)' field :sentry_project_name, GraphQL::STRING_TYPE, method: :project_name, null: false, - description: "Name of the project affected by the error" + description: 'Name of the project affected by the error' field :sentry_project_slug, GraphQL::STRING_TYPE, method: :project_slug, null: false, - description: "Slug of the project affected by the error" + description: 'Slug of the project affected by the error' field :short_id, GraphQL::STRING_TYPE, null: false, - description: "Short ID (Sentry ID) of the error" + description: 'Short ID (Sentry ID) of the error' field :status, Types::ErrorTracking::SentryErrorStatusEnum, null: false, - description: "Status of the error" + description: 'Status of the error' field :frequency, [Types::ErrorTracking::SentryErrorFrequencyType], null: false, - description: "Last 24hr stats of the error" + description: 'Last 24hr stats of the error' field :first_release_last_commit, GraphQL::STRING_TYPE, null: true, - description: "Commit the error was first seen" + description: 'Commit the error was first seen' field :last_release_last_commit, GraphQL::STRING_TYPE, null: true, - description: "Commit the error was last seen" + description: 'Commit the error was last seen' field :first_release_short_version, GraphQL::STRING_TYPE, null: true, - description: "Release version the error was first seen" + description: 'Release version the error was first seen' field :last_release_short_version, GraphQL::STRING_TYPE, null: true, - description: "Release version the error was last seen" + description: 'Release version the error was last seen' field :gitlab_commit, GraphQL::STRING_TYPE, null: true, - description: "GitLab commit SHA attributed to the Error based on the release version" + description: 'GitLab commit SHA attributed to the Error based on the release version' field :gitlab_commit_path, GraphQL::STRING_TYPE, null: true, - description: "Path to the GitLab page for the GitLab commit attributed to the error" + description: 'Path to the GitLab page for the GitLab commit attributed to the error' + field :gitlab_issue_path, GraphQL::STRING_TYPE, + method: :gitlab_issue, + null: true, + description: 'URL of GitLab Issue' + field :tags, Types::ErrorTracking::SentryErrorTagsType, + null: false, + description: 'Tags associated with the Sentry Error' def first_seen DateTime.parse(object.first_seen) diff --git a/app/graphql/types/error_tracking/sentry_error_tags_type.rb b/app/graphql/types/error_tracking/sentry_error_tags_type.rb new file mode 100644 index 00000000000..e6d96571561 --- /dev/null +++ b/app/graphql/types/error_tracking/sentry_error_tags_type.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Types + module ErrorTracking + # rubocop: disable Graphql/AuthorizeTypes + class SentryErrorTagsType < ::Types::BaseObject + graphql_name 'SentryErrorTags' + description 'State of a Sentry error' + + field :level, GraphQL::STRING_TYPE, + null: true, + description: "Severity level of the Sentry Error" + field :logger, GraphQL::STRING_TYPE, + null: true, + description: "Logger of the Sentry Error" + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index c9fb28d0299..7c0f4da355d 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -47,7 +47,7 @@ module BlobHelper def edit_blob_button(project = @project, ref = @ref, path = @path, options = {}) return unless blob = readable_blob(options, path, project, ref) - common_classes = "btn btn-primary js-edit-blob #{options[:extra_class]}" + common_classes = "btn btn-primary js-edit-blob ml-2 #{options[:extra_class]}" edit_button_tag(blob, common_classes, @@ -62,7 +62,7 @@ module BlobHelper return unless blob = readable_blob(options, path, project, ref) edit_button_tag(blob, - 'btn btn-inverted btn-primary ide-edit-button', + 'btn btn-inverted btn-primary ide-edit-button ml-2', _('Web IDE'), ide_edit_path(project, ref, path, options), project, diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 63f216c7af5..bf189d0f517 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -4,6 +4,7 @@ module Clusters module Applications class Ingress < ApplicationRecord VERSION = '1.22.1' + MODSECURITY_LOG_CONTAINER_NAME = 'modsecurity-log' self.table_name = 'clusters_applications_ingress' @@ -85,7 +86,7 @@ module Clusters }, "extraContainers" => [ { - "name" => "modsecurity-log", + "name" => MODSECURITY_LOG_CONTAINER_NAME, "image" => "busybox", "args" => [ "/bin/sh", diff --git a/app/models/project.rb b/app/models/project.rb index cd191589351..c0103190892 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1938,6 +1938,8 @@ class Project < ApplicationRecord .append(key: 'GITLAB_CI', value: 'true') .append(key: 'CI_SERVER_URL', value: Gitlab.config.gitlab.url) .append(key: 'CI_SERVER_HOST', value: Gitlab.config.gitlab.host) + .append(key: 'CI_SERVER_PORT', value: Gitlab.config.gitlab.port.to_s) + .append(key: 'CI_SERVER_PROTOCOL', value: Gitlab.config.gitlab.protocol) .append(key: 'CI_SERVER_NAME', value: 'GitLab') .append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION) .append(key: 'CI_SERVER_VERSION_MAJOR', value: Gitlab.version_info.major.to_s) diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml index 77245114772..76a9d3df5d7 100644 --- a/app/views/projects/blob/_header.html.haml +++ b/app/views/projects/blob/_header.html.haml @@ -2,18 +2,16 @@ .js-file-title.file-title-flex-parent = render 'projects/blob/header_content', blob: blob - .file-actions + .file-actions< = render 'projects/blob/viewer_switcher', blob: blob unless blame - - .btn-group{ role: "group" }< - = edit_blob_button - = ide_edit_button - .btn-group{ role: "group" }< + = edit_blob_button + = ide_edit_button + .btn-group.ml-2{ role: "group" }> = render_if_exists 'projects/blob/header_file_locks_link' - if current_user = replace_blob_link = delete_blob_link - .btn-group{ role: "group" }< + .btn-group.ml-2{ role: "group" } = copy_blob_source_button(blob) unless blame = open_raw_blob_button(blob) = download_blob_button(blob) diff --git a/app/views/projects/blob/_viewer_switcher.html.haml b/app/views/projects/blob/_viewer_switcher.html.haml index 6a521069418..5e0d70b2ca9 100644 --- a/app/views/projects/blob/_viewer_switcher.html.haml +++ b/app/views/projects/blob/_viewer_switcher.html.haml @@ -2,7 +2,7 @@ - simple_viewer = blob.simple_viewer - rich_viewer = blob.rich_viewer - .btn-group.js-blob-viewer-switcher{ role: "group" } + .btn-group.js-blob-viewer-switcher.ml-2{ role: "group" }> - simple_label = "Display #{simple_viewer.switcher_title}" %button.btn.btn-default.btn-sm.js-blob-viewer-switch-btn.has-tooltip{ 'aria-label' => simple_label, title: simple_label, data: { viewer: 'simple', container: 'body' } }> = icon(simple_viewer.switcher_icon) diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index d5c1a1bee6d..d74030c566f 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -22,6 +22,8 @@ - if user == current_user %span.badge.badge-success.prepend-left-5= _("It's you") + = render_if_exists 'shared/members/ee/license_badge', user: user, group: @group + - if user.blocked? %label.badge.badge-danger %strong= _("Blocked") diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index 9492cfe217c..1ab2fd6023f 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -6,6 +6,7 @@ class AuthorizedProjectsWorker feature_category :authentication_and_authorization latency_sensitive_worker! + weight 2 # This is a workaround for a Ruby 2.3.7 bug. rspec-mocks cannot restore the # visibility of prepended modules. See https://github.com/rspec/rspec-mocks/issues/1231 diff --git a/app/workers/chat_notification_worker.rb b/app/workers/chat_notification_worker.rb index 6162dcf9d38..f23c787559f 100644 --- a/app/workers/chat_notification_worker.rb +++ b/app/workers/chat_notification_worker.rb @@ -8,6 +8,8 @@ class ChatNotificationWorker sidekiq_options retry: false feature_category :chatops latency_sensitive_worker! + weight 2 + # TODO: break this into multiple jobs # as the `responder` uses external dependencies # See https://gitlab.com/gitlab-com/gl-infra/scalability/issues/34 diff --git a/app/workers/concerns/self_monitoring_project_worker.rb b/app/workers/concerns/self_monitoring_project_worker.rb index 44dd6866fad..1796e2441f2 100644 --- a/app/workers/concerns/self_monitoring_project_worker.rb +++ b/app/workers/concerns/self_monitoring_project_worker.rb @@ -9,6 +9,7 @@ module SelfMonitoringProjectWorker # Other Functionality. Metrics seems to be the closest feature_category for # this worker. feature_category :metrics + weight 2 end LEASE_TIMEOUT = 15.minutes.to_i diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb index 506215ca9ed..266c8021761 100644 --- a/app/workers/concerns/worker_attributes.rb +++ b/app/workers/concerns/worker_attributes.rb @@ -7,6 +7,24 @@ module WorkerAttributes # `worker_resource_boundary` attribute VALID_RESOURCE_BOUNDARIES = [:memory, :cpu, :unknown].freeze + NAMESPACE_WEIGHTS = { + auto_devops: 2, + auto_merge: 3, + chaos: 2, + deployment: 3, + mail_scheduler: 2, + notifications: 2, + pipeline_cache: 3, + pipeline_creation: 4, + pipeline_default: 3, + pipeline_hooks: 2, + pipeline_processing: 5, + + # EE-specific + epics: 2, + incident_management: 2 + }.stringify_keys.freeze + class_methods do def feature_category(value) raise "Invalid category. Use `feature_category_not_owned!` to mark a worker as not owned" if value == :not_owned @@ -70,6 +88,16 @@ module WorkerAttributes worker_attributes[:resource_boundary] || :unknown end + def weight(value) + worker_attributes[:weight] = value + end + + def get_weight + worker_attributes[:weight] || + NAMESPACE_WEIGHTS[queue_namespace] || + 1 + end + protected # Returns a worker attribute declared on this class or its parent class. diff --git a/app/workers/create_evidence_worker.rb b/app/workers/create_evidence_worker.rb index 027dbd2f101..e6fbf59d702 100644 --- a/app/workers/create_evidence_worker.rb +++ b/app/workers/create_evidence_worker.rb @@ -4,6 +4,7 @@ class CreateEvidenceWorker include ApplicationWorker feature_category :release_governance + weight 2 def perform(release_id) release = Release.find_by_id(release_id) diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb index fc36a2adccd..2043c3c8e77 100644 --- a/app/workers/create_gpg_signature_worker.rb +++ b/app/workers/create_gpg_signature_worker.rb @@ -4,6 +4,7 @@ class CreateGpgSignatureWorker include ApplicationWorker feature_category :source_code_management + weight 2 # rubocop: disable CodeReuse/ActiveRecord def perform(commit_shas, project_id) diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index b56bf4ed833..c2b1e642604 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -5,6 +5,7 @@ class EmailReceiverWorker feature_category :issue_tracking latency_sensitive_worker! + weight 2 def perform(raw) return unless Gitlab::IncomingEmail.enabled? diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index f523f5953e1..be66e2b1188 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -8,6 +8,7 @@ class EmailsOnPushWorker feature_category :source_code_management latency_sensitive_worker! worker_resource_boundary :cpu + weight 2 def perform(project_id, recipients, push_data, options = {}) options.symbolize_keys! diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb index 57e64570c09..bd2225e6d7c 100644 --- a/app/workers/gitlab_shell_worker.rb +++ b/app/workers/gitlab_shell_worker.rb @@ -6,6 +6,7 @@ class GitlabShellWorker feature_category :source_code_management latency_sensitive_worker! + weight 2 def perform(action, *arg) Gitlab::GitalyClient::NamespaceService.allow do diff --git a/app/workers/import_issues_csv_worker.rb b/app/workers/import_issues_csv_worker.rb index d2733dc5f56..7c5584146ca 100644 --- a/app/workers/import_issues_csv_worker.rb +++ b/app/workers/import_issues_csv_worker.rb @@ -5,6 +5,7 @@ class ImportIssuesCsvWorker feature_category :issue_tracking worker_resource_boundary :cpu + weight 2 sidekiq_retries_exhausted do |job| Upload.find(job['args'][2]).destroy diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb index 573efdf9fb1..e1c2eefbf0f 100644 --- a/app/workers/invalid_gpg_signature_update_worker.rb +++ b/app/workers/invalid_gpg_signature_update_worker.rb @@ -4,6 +4,7 @@ class InvalidGpgSignatureUpdateWorker include ApplicationWorker feature_category :source_code_management + weight 2 # rubocop: disable CodeReuse/ActiveRecord def perform(gpg_key_id) diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index ed88c57e8d4..48bc205113f 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -5,6 +5,7 @@ class MergeWorker feature_category :source_code_management latency_sensitive_worker! + weight 5 def perform(merge_request_id, current_user_id, params) params = params.with_indifferent_access diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb index af9ca332d3c..d696165b447 100644 --- a/app/workers/new_issue_worker.rb +++ b/app/workers/new_issue_worker.rb @@ -7,6 +7,7 @@ class NewIssueWorker feature_category :issue_tracking latency_sensitive_worker! worker_resource_boundary :cpu + weight 2 def perform(issue_id, user_id) return unless objects_found?(issue_id, user_id) diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb index aa3f85c157b..e31ddae1f13 100644 --- a/app/workers/new_merge_request_worker.rb +++ b/app/workers/new_merge_request_worker.rb @@ -7,6 +7,7 @@ class NewMergeRequestWorker feature_category :source_code_management latency_sensitive_worker! worker_resource_boundary :cpu + weight 2 def perform(merge_request_id, user_id) return unless objects_found?(merge_request_id, user_id) diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb index 2a5988a7e32..b446e376007 100644 --- a/app/workers/new_note_worker.rb +++ b/app/workers/new_note_worker.rb @@ -6,6 +6,7 @@ class NewNoteWorker feature_category :issue_tracking latency_sensitive_worker! worker_resource_boundary :cpu + weight 2 # Keep extra parameter to preserve backwards compatibility with # old `NewNoteWorker` jobs (can remove later) diff --git a/app/workers/new_release_worker.rb b/app/workers/new_release_worker.rb index a3a882f9343..edfdb2d7aff 100644 --- a/app/workers/new_release_worker.rb +++ b/app/workers/new_release_worker.rb @@ -5,6 +5,7 @@ class NewReleaseWorker queue_namespace :notifications feature_category :release_orchestration + weight 2 def perform(release_id) release = Release.preloaded.find_by_id(release_id) diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 334a98a0017..d5038f1152b 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -6,6 +6,7 @@ class PostReceive feature_category :source_code_management latency_sensitive_worker! worker_resource_boundary :cpu + weight 5 def perform(gl_repository, identifier, changes, push_options = {}) project, repo_type = Gitlab::GlRepository.parse(gl_repository) diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index 36af51d859e..ca2896946c9 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -12,6 +12,7 @@ class ProcessCommitWorker feature_category :source_code_management latency_sensitive_worker! + weight 3 # project_id - The ID of the project this commit belongs to. # user_id - The ID of the user that pushed the commit. diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb index fd182125c07..ddf5c31a1c2 100644 --- a/app/workers/rebase_worker.rb +++ b/app/workers/rebase_worker.rb @@ -6,6 +6,7 @@ class RebaseWorker include ApplicationWorker feature_category :source_code_management + weight 2 def perform(merge_request_id, current_user_id, skip_ci = false) current_user = User.find(current_user_id) diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb index 8bc19230caf..706131d4e4b 100644 --- a/app/workers/remote_mirror_notification_worker.rb +++ b/app/workers/remote_mirror_notification_worker.rb @@ -4,6 +4,7 @@ class RemoteMirrorNotificationWorker include ApplicationWorker feature_category :source_code_management + weight 2 def perform(remote_mirror_id) remote_mirror = RemoteMirror.find_by_id(remote_mirror_id) diff --git a/app/workers/update_external_pull_requests_worker.rb b/app/workers/update_external_pull_requests_worker.rb index 8b0952528fa..e363b33f1b9 100644 --- a/app/workers/update_external_pull_requests_worker.rb +++ b/app/workers/update_external_pull_requests_worker.rb @@ -4,6 +4,7 @@ class UpdateExternalPullRequestsWorker include ApplicationWorker feature_category :source_code_management + weight 3 def perform(project_id, user_id, ref) project = Project.find_by_id(project_id) diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb index acb95353983..ec9739e8a11 100644 --- a/app/workers/update_merge_requests_worker.rb +++ b/app/workers/update_merge_requests_worker.rb @@ -6,6 +6,7 @@ class UpdateMergeRequestsWorker feature_category :source_code_management latency_sensitive_worker! worker_resource_boundary :cpu + weight 3 LOG_TIME_THRESHOLD = 90 # seconds diff --git a/changelogs/unreleased/14707-add-waf-anomaly-summary-service.yml b/changelogs/unreleased/14707-add-waf-anomaly-summary-service.yml new file mode 100644 index 00000000000..18a9bfb67ab --- /dev/null +++ b/changelogs/unreleased/14707-add-waf-anomaly-summary-service.yml @@ -0,0 +1,5 @@ +--- +title: Add WAF Anomaly Summary service +merge_request: 22736 +author: +type: added diff --git a/changelogs/unreleased/194164-add-missing-detailed-sentry-error-graphql.yml b/changelogs/unreleased/194164-add-missing-detailed-sentry-error-graphql.yml new file mode 100644 index 00000000000..163309e32d1 --- /dev/null +++ b/changelogs/unreleased/194164-add-missing-detailed-sentry-error-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Add tags, external_base_url, gitlab_issue to Sentry Detailed Error graphql +merge_request: 23483 +author: +type: added diff --git a/changelogs/unreleased/197500-edit-and-web-ide-look-like-segmented-control-in-file-header.yml b/changelogs/unreleased/197500-edit-and-web-ide-look-like-segmented-control-in-file-header.yml new file mode 100644 index 00000000000..7eb6ea5f521 --- /dev/null +++ b/changelogs/unreleased/197500-edit-and-web-ide-look-like-segmented-control-in-file-header.yml @@ -0,0 +1,5 @@ +--- +title: Remove button group for edit and web ide in file header +merge_request: 23291 +author: +type: other diff --git a/changelogs/unreleased/23296-add-ci-variables-server-port-protocol.yml b/changelogs/unreleased/23296-add-ci-variables-server-port-protocol.yml new file mode 100644 index 00000000000..5ee9fbe540e --- /dev/null +++ b/changelogs/unreleased/23296-add-ci-variables-server-port-protocol.yml @@ -0,0 +1,5 @@ +--- +title: Add CI variables to provide GitLab port and protocol +merge_request: 23296 +author: Aidin Abedi +type: added diff --git a/changelogs/unreleased/broadcast-api-auth.yml b/changelogs/unreleased/broadcast-api-auth.yml new file mode 100644 index 00000000000..57e0097785d --- /dev/null +++ b/changelogs/unreleased/broadcast-api-auth.yml @@ -0,0 +1,5 @@ +--- +title: Allow users to read broadcast messages via API +merge_request: 23298 +author: Rajendra Kadam +type: changed diff --git a/changelogs/unreleased/fix-missing-apply-sugegstion-project-setting.yml b/changelogs/unreleased/fix-missing-apply-sugegstion-project-setting.yml new file mode 100644 index 00000000000..d084148fbd8 --- /dev/null +++ b/changelogs/unreleased/fix-missing-apply-sugegstion-project-setting.yml @@ -0,0 +1,5 @@ +--- +title: Add accidentally deleted project config for custom apply suggestions +merge_request: 23687 +author: Fabio Huser +type: fixed diff --git a/changelogs/unreleased/integrate-gitaly-disk_statistics-grpc.yml b/changelogs/unreleased/integrate-gitaly-disk_statistics-grpc.yml new file mode 100644 index 00000000000..ef90fc89d8d --- /dev/null +++ b/changelogs/unreleased/integrate-gitaly-disk_statistics-grpc.yml @@ -0,0 +1,5 @@ +--- +title: "Support retrieval of disk statistics from Gitaly" +merge_request: 22226 +author: Nels Nelson +type: added diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index 9135073eaea..e7bab1c8fa3 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -15,6 +15,10 @@ unless Gitlab::Runtime.sidekiq? data end + # This isn't a user-reachable controller; we use it to check for a + # valid CSRF token in the API + config.lograge.ignore_actions = ['Gitlab::RequestForgeryProtection::Controller#index'] + # Add request parameters to log output config.lograge.custom_options = lambda do |event| params = event.payload[:params] diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb index aac6d418a92..593f818e434 100644 --- a/config/routes/git_http.rb +++ b/config/routes/git_http.rb @@ -1,43 +1,51 @@ -scope(path: '*namespace_id/:project_id', +concern :gitactionable do + scope(controller: :git_http) do + get '/info/refs', action: :info_refs + post '/git-upload-pack', action: :git_upload_pack + post '/git-receive-pack', action: :git_receive_pack + end +end + +concern :lfsable do + # Git LFS API (metadata) + scope(path: 'info/lfs/objects', controller: :lfs_api) do + post :batch + post '/', action: :deprecated + get '/*oid', action: :deprecated + end + + scope(path: 'info/lfs') do + resources :lfs_locks, controller: :lfs_locks_api, path: 'locks' do + post :unlock, on: :member + post :verify, on: :collection + end + end + + # GitLab LFS object storage + scope(path: 'gitlab-lfs/objects/*oid', controller: :lfs_storage, constraints: { oid: /[a-f0-9]{64}/ }) do + get '/', action: :download + + scope constraints: { size: /[0-9]+/ } do + put '/*size/authorize', action: :upload_authorize + put '/*size', action: :upload_finalize + end + end +end + +scope(path: '*namespace_id/:repository_id', format: nil, constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do - scope(constraints: { project_id: Gitlab::PathRegex.project_git_route_regex }, module: :projects) do - # Git HTTP clients ('git clone' etc.) - scope(controller: :git_http) do - get '/info/refs', action: :info_refs - post '/git-upload-pack', action: :git_upload_pack - post '/git-receive-pack', action: :git_receive_pack - end - - # Git LFS API (metadata) - scope(path: 'info/lfs/objects', controller: :lfs_api) do - post :batch - post '/', action: :deprecated - get '/*oid', action: :deprecated - end - - scope(path: 'info/lfs') do - resources :lfs_locks, controller: :lfs_locks_api, path: 'locks' do - post :unlock, on: :member - post :verify, on: :collection - end - end - - # GitLab LFS object storage - scope(path: 'gitlab-lfs/objects/*oid', controller: :lfs_storage, constraints: { oid: /[a-f0-9]{64}/ }) do - get '/', action: :download - - scope constraints: { size: /[0-9]+/ } do - put '/*size/authorize', action: :upload_authorize - put '/*size', action: :upload_finalize - end + scope(constraints: { repository_id: Gitlab::PathRegex.project_git_route_regex }) do + scope(module: :repositories) do + concerns :gitactionable + concerns :lfsable end end # Redirect /group/project.wiki.git to the project wiki - scope(format: true, constraints: { project_id: Gitlab::PathRegex.project_wiki_git_route_regex, format: :git }) do + scope(format: true, constraints: { repository_id: Gitlab::PathRegex.project_wiki_git_route_regex, format: :git }) do wiki_redirect = redirect do |params, request| - project_id = params[:project_id].delete_suffix('.wiki') + project_id = params[:repository_id].delete_suffix('.wiki') path = [params[:namespace_id], project_id, 'wikis'].join('/') path << "?#{request.query_string}" unless request.query_string.blank? path @@ -47,7 +55,7 @@ scope(path: '*namespace_id/:project_id', end # Redirect /group/project/info/refs to /group/project.git/info/refs - scope(constraints: { project_id: Gitlab::PathRegex.project_route_regex }) do + scope(constraints: { repository_id: Gitlab::PathRegex.project_route_regex }) do # Allow /info/refs, /info/refs?service=git-upload-pack, and # /info/refs?service=git-receive-pack, but nothing else. # @@ -58,7 +66,7 @@ scope(path: '*namespace_id/:project_id', end ref_redirect = redirect do |params, request| - path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs" + path = "#{params[:namespace_id]}/#{params[:repository_id]}.git/info/refs" path << "?#{request.query_string}" unless request.query_string.blank? path end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index e341c91899b..4f8d93e66bb 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -1,7 +1,12 @@ +# 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. # @@ -17,116 +22,217 @@ # to perform) is: # # chance = (queue weight / total weight of all queues) * 100 +--- :queues: - - [post_receive, 5] - - [merge, 5] - - [update_merge_requests, 3] - - [process_commit, 3] - - [new_note, 2] - - [new_issue, 2] - - [notifications, 2] - - [new_merge_request, 2] - - [pipeline_processing, 5] - - [pipeline_creation, 4] - - [pipeline_default, 3] - - [pipeline_cache, 3] - - [deployment, 3] - - [auto_merge, 3] - - [pipeline_hooks, 2] - - [gitlab_shell, 2] - - [email_receiver, 2] - - [emails_on_push, 2] - - [mailers, 2] - - [mail_scheduler, 2] - - [invalid_gpg_signature_update, 2] - - [create_gpg_signature, 2] - - [rebase, 2] - - [upload_checksum, 1] - - [repository_fork, 1] - - [repository_import, 1] - - [github_importer, 1] - - [github_import_advance_stage, 1] - - [project_service, 1] - - [delete_user, 1] - - [todos_destroyer, 1] - - [delete_merged_branches, 1] - - [authorized_projects, 2] - - [expire_build_instance_artifacts, 1] - - [group_destroy, 1] - - [irker, 1] - - [namespaceless_project_destroy, 1] - - [project_cache, 1] - - [project_destroy, 1] - - [project_export, 1] - - [web_hook, 1] - - [repository_check, 1] - - [git_garbage_collect, 1] - - [reactive_caching, 1] - - [cronjob, 1] - - [default, 1] - - [pages, 1] - - [system_hook_push, 1] - - [propagate_service_template, 1] - - [background_migration, 1] - - [gcp_cluster, 1] - - [project_migrate_hashed_storage, 1] - - [project_rollback_hashed_storage, 1] - - [hashed_storage, 1] - - [pages_domain_verification, 1] - - [pages_domain_ssl_renewal, 1] - - [object_storage_upload, 1] - - [object_storage, 1] - - [file_hook, 1] - - [pipeline_background, 1] - - [repository_update_remote_mirror, 1] - - [repository_remove_remote, 1] - - [create_note_diff_file, 1] - - [delete_diff_files, 1] - - [detect_repository_languages, 1] - - [auto_devops, 2] - - [container_repository, 1] - - [object_pool, 1] - - [repository_cleanup, 1] - - [delete_stored_files, 1] - - [remote_mirror_notification, 2] - - [project_daily_statistics, 1] - - [import_issues_csv, 2] - - [chat_notification, 2] - - [migrate_external_diffs, 1] - - [update_project_statistics, 1] - - [phabricator_import_import_tasks, 1] - - [update_namespace_statistics, 1] - - [chaos, 2] - - [create_evidence, 2] - - [group_export, 1] - - [self_monitoring_project_create, 2] - - [self_monitoring_project_delete, 2] - - [error_tracking_issue_link, 2] - - [merge_request_mergeability_check, 5] - - # EE-specific queues - - [analytics, 1] - - [ldap_group_sync, 2] - - [create_github_webhook, 2] - - [geo, 1] - - [repository_update_mirror, 1] - - [repository_push_audit_event, 1] - - [new_epic, 2] - - [project_import_schedule, 1] - - [project_update_repository_storage, 1] - - [admin_emails, 1] - - [elastic_batch_project_indexer, 1] - - [elastic_indexer, 1] - - [elastic_full_index, 1] - - [elastic_commit_indexer, 1] - - [elastic_namespace_indexer, 1] - - [elastic_namespace_rollout, 1] - - [export_csv, 1] - - [incident_management, 2] - - [jira_connect, 1] - - [update_external_pull_requests, 3] - - [refresh_license_compliance_checks, 2] - - [design_management_new_version, 1] - - [epics, 2] - - [personal_access_tokens, 1] - - [adjourned_project_deletion, 1] +- - adjourned_project_deletion + - 1 +- - admin_emails + - 1 +- - authorized_projects + - 2 +- - auto_devops + - 2 +- - auto_merge + - 3 +- - background_migration + - 1 +- - chaos + - 2 +- - chat_notification + - 2 +- - container_repository + - 1 +- - create_evidence + - 2 +- - create_github_webhook + - 2 +- - create_gpg_signature + - 2 +- - create_note_diff_file + - 1 +- - cronjob + - 1 +- - default + - 1 +- - delete_diff_files + - 1 +- - delete_merged_branches + - 1 +- - delete_stored_files + - 1 +- - delete_user + - 1 +- - deployment + - 3 +- - design_management_new_version + - 1 +- - detect_repository_languages + - 1 +- - elastic_batch_project_indexer + - 1 +- - elastic_commit_indexer + - 1 +- - elastic_full_index + - 1 +- - elastic_indexer + - 1 +- - elastic_namespace_indexer + - 1 +- - elastic_namespace_rollout + - 1 +- - email_receiver + - 2 +- - emails_on_push + - 2 +- - epics + - 2 +- - error_tracking_issue_link + - 1 +- - expire_build_instance_artifacts + - 1 +- - export_csv + - 1 +- - file_hook + - 1 +- - gcp_cluster + - 1 +- - geo + - 1 +- - git_garbage_collect + - 1 +- - github_import_advance_stage + - 1 +- - github_importer + - 1 +- - gitlab_shell + - 2 +- - group_destroy + - 1 +- - group_export + - 1 +- - hashed_storage + - 1 +- - import_issues_csv + - 2 +- - incident_management + - 2 +- - invalid_gpg_signature_update + - 2 +- - irker + - 1 +- - jira_connect + - 1 +- - ldap_group_sync + - 2 +- - mail_scheduler + - 2 +- - mailers + - 2 +- - merge + - 5 +- - merge_request_mergeability_check + - 1 +- - migrate_external_diffs + - 1 +- - namespaceless_project_destroy + - 1 +- - new_epic + - 2 +- - new_issue + - 2 +- - new_merge_request + - 2 +- - new_note + - 2 +- - notifications + - 2 +- - object_pool + - 1 +- - object_storage + - 1 +- - pages + - 1 +- - pages_domain_ssl_renewal + - 1 +- - pages_domain_verification + - 1 +- - personal_access_tokens + - 1 +- - phabricator_import_import_tasks + - 1 +- - pipeline_background + - 1 +- - pipeline_cache + - 3 +- - pipeline_creation + - 4 +- - pipeline_default + - 3 +- - pipeline_hooks + - 2 +- - pipeline_processing + - 5 +- - post_receive + - 5 +- - process_commit + - 3 +- - project_cache + - 1 +- - project_daily_statistics + - 1 +- - project_destroy + - 1 +- - project_export + - 1 +- - project_import_schedule + - 1 +- - project_service + - 1 +- - project_update_repository_storage + - 1 +- - propagate_service_template + - 1 +- - reactive_caching + - 1 +- - rebase + - 2 +- - refresh_license_compliance_checks + - 2 +- - remote_mirror_notification + - 2 +- - repository_check + - 1 +- - repository_cleanup + - 1 +- - repository_fork + - 1 +- - repository_import + - 1 +- - repository_push_audit_event + - 1 +- - repository_remove_remote + - 1 +- - repository_update_mirror + - 1 +- - repository_update_remote_mirror + - 1 +- - self_monitoring_project_create + - 2 +- - self_monitoring_project_delete + - 2 +- - system_hook_push + - 1 +- - todos_destroyer + - 1 +- - update_external_pull_requests + - 3 +- - update_merge_requests + - 3 +- - update_namespace_statistics + - 1 +- - update_project_statistics + - 1 +- - upload_checksum + - 1 +- - web_hook + - 1 diff --git a/db/migrate/20200124053531_add_source_to_import_failures.rb b/db/migrate/20200124053531_add_source_to_import_failures.rb new file mode 100644 index 00000000000..532d5803c74 --- /dev/null +++ b/db/migrate/20200124053531_add_source_to_import_failures.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddSourceToImportFailures < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column :import_failures, :source, :string, limit: 128 + end +end diff --git a/db/schema.rb b/db/schema.rb index 8ad4e080fe7..8f8a44f1519 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_01_23_155929) do +ActiveRecord::Schema.define(version: 2020_01_24_053531) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -2046,6 +2046,7 @@ ActiveRecord::Schema.define(version: 2020_01_23_155929) do t.string "exception_message", limit: 255 t.integer "retry_count" t.integer "group_id" + t.string "source", limit: 128 t.index ["correlation_id_value"], name: "index_import_failures_on_correlation_id_value" t.index ["group_id"], name: "index_import_failures_on_group_id_not_null", where: "(group_id IS NOT NULL)" t.index ["project_id"], name: "index_import_failures_on_project_id_not_null", where: "(project_id IS NOT NULL)" diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md index ce68eec87ff..6a9a0ba745a 100644 --- a/doc/api/broadcast_messages.md +++ b/doc/api/broadcast_messages.md @@ -4,7 +4,7 @@ Broadcast messages API operates on [broadcast messages](../user/admin_area/broadcast_messages.md). -The broadcast message API is only accessible to administrators. All requests by: +As of GitLab 12.8, GET requests do not require authentication. All other broadcast message API endpoints are accessible only to administrators. Non-GET requests by: - Guests will result in `401 Unauthorized`. - Regular users will result in `403 Forbidden`. @@ -20,7 +20,7 @@ GET /broadcast_messages Example request: ```sh -curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/broadcast_messages +curl https://gitlab.example.com/api/v4/broadcast_messages ``` Example response: @@ -57,7 +57,7 @@ Parameters: Example request: ```sh -curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/broadcast_messages/1 +curl https://gitlab.example.com/api/v4/broadcast_messages/1 ``` Example response: diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 4e083142514..39b34e72e24 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -6065,6 +6065,11 @@ type SentryDetailedError { """ culprit: String! + """ + External Base URL of the Sentry Instance + """ + externalBaseUrl: String! + """ External URL of the error """ @@ -6100,6 +6105,11 @@ type SentryDetailedError { """ gitlabCommitPath: String + """ + URL of GitLab Issue + """ + gitlabIssuePath: String + """ ID (global ID) of the error """ @@ -6155,6 +6165,11 @@ type SentryDetailedError { """ status: SentryErrorStatus! + """ + Tags associated with the Sentry Error + """ + tags: SentryErrorTags! + """ Title of the error """ @@ -6208,6 +6223,21 @@ enum SentryErrorStatus { UNRESOLVED } +""" +State of a Sentry error +""" +type SentryErrorTags { + """ + Severity level of the Sentry Error + """ + level: String + + """ + Logger of the Sentry Error + """ + logger: String +} + """ Represents a snippet entry """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 5c1c05d6d4e..2d2bcaf32bd 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -16746,6 +16746,24 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "externalBaseUrl", + "description": "External Base URL of the Sentry Instance", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "externalUrl", "description": "External URL of the error", @@ -16864,6 +16882,20 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "gitlabIssuePath", + "description": "URL of GitLab Issue", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "id", "description": "ID (global ID) of the error", @@ -17050,6 +17082,24 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tags", + "description": "Tags associated with the Sentry Error", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SentryErrorTags", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "title", "description": "Title of the error", @@ -17196,6 +17246,47 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "SentryErrorTags", + "description": "State of a Sentry error", + "fields": [ + { + "name": "level", + "description": "Severity level of the Sentry Error", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "logger", + "description": "Logger of the Sentry Error", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "GrafanaIntegration", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 210554243a8..790e55d437f 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -923,6 +923,7 @@ Autogenerated return type of RemoveAwardEmoji | --- | ---- | ---------- | | `count` | Int! | Count of occurrences | | `culprit` | String! | Culprit of the error | +| `externalBaseUrl` | String! | External Base URL of the Sentry Instance | | `externalUrl` | String! | External URL of the error | | `firstReleaseLastCommit` | String | Commit the error was first seen | | `firstReleaseShortVersion` | String | Release version the error was first seen | @@ -930,6 +931,7 @@ Autogenerated return type of RemoveAwardEmoji | `frequency` | SentryErrorFrequency! => Array | Last 24hr stats of the error | | `gitlabCommit` | String | GitLab commit SHA attributed to the Error based on the release version | | `gitlabCommitPath` | String | Path to the GitLab page for the GitLab commit attributed to the error | +| `gitlabIssuePath` | String | URL of GitLab Issue | | `id` | ID! | ID (global ID) of the error | | `lastReleaseLastCommit` | String | Commit the error was last seen | | `lastReleaseShortVersion` | String | Release version the error was last seen | @@ -941,6 +943,7 @@ Autogenerated return type of RemoveAwardEmoji | `sentryProjectSlug` | String! | Slug of the project affected by the error | | `shortId` | String! | Short ID (Sentry ID) of the error | | `status` | SentryErrorStatus! | Status of the error | +| `tags` | SentryErrorTags! | Tags associated with the Sentry Error | | `title` | String! | Title of the error | | `type` | String! | Type of the error | | `userCount` | Int! | Count of users affected by the error | @@ -952,6 +955,15 @@ Autogenerated return type of RemoveAwardEmoji | `count` | Int! | Count of errors received since the previously recorded time | | `time` | Time! | Time the error frequency stats were recorded | +## SentryErrorTags + +State of a Sentry error + +| Name | Type | Description | +| --- | ---- | ---------- | +| `level` | String | Severity level of the Sentry Error | +| `logger` | String | Logger of the Sentry Error | + ## Snippet Represents a snippet entry diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 19fcca36604..8f1ce48906a 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -292,6 +292,8 @@ export CI_RUNNER_TAGS="docker, linux" export CI_SERVER="yes" export CI_SERVER_URL="https://example.com" export CI_SERVER_HOST="example.com" +export CI_SERVER_PORT="443" +export CI_SERVER_PROTOCOL="https" export CI_SERVER_NAME="GitLab" export CI_SERVER_REVISION="70606bf" export CI_SERVER_VERSION="8.9.0" @@ -686,6 +688,10 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then ++ CI_SERVER_URL=https://gitlab.com:3000 ++ export CI_SERVER_HOST=gitlab.com ++ CI_SERVER_HOST=gitlab.com +++ export CI_SERVER_PORT=3000 +++ CI_SERVER_PORT=3000 +++ export CI_SERVER_PROTOCOL=https +++ CI_SERVER_PROTOCOL=https ++ export CI_SERVER_NAME=GitLab ++ CI_SERVER_NAME=GitLab ++ export CI_SERVER_VERSION=12.6.0-pre diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md index 24a16fe6a70..5cc93427d42 100644 --- a/doc/ci/variables/predefined_variables.md +++ b/doc/ci/variables/predefined_variables.md @@ -114,6 +114,8 @@ future GitLab releases.** | `CI_SERVER` | all | all | Mark that job is executed in CI environment | | `CI_SERVER_URL` | 12.7 | all | The base URL of the GitLab instance, including protocol and port (like `https://gitlab.example.com:8080`) | | `CI_SERVER_HOST` | 12.1 | all | Host component of the GitLab instance URL, without protocol and port (like `gitlab.example.com`) | +| `CI_SERVER_PORT` | 12.8 | all | Port component of the GitLab instance URL, without host and protocol (like `3000`) | +| `CI_SERVER_PROTOCOL` | 12.8 | all | Protocol component of the GitLab instance URL, without host and port (like `https`) | | `CI_SERVER_NAME` | all | all | The name of CI server that is used to coordinate jobs | | `CI_SERVER_REVISION` | all | all | GitLab revision that is used to schedule jobs | | `CI_SERVER_VERSION` | all | all | GitLab version that is used to schedule jobs | diff --git a/doc/user/discussions/img/suggestion_code_block_editor_v12_8.png b/doc/user/discussions/img/suggestion_code_block_editor_v12_8.png new file mode 100644 index 0000000000000000000000000000000000000000..927b4f812a589d6b622187f337f4342e619596a3 GIT binary patch literal 9917 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 literal 0 HcmV?d00001 diff --git a/doc/user/discussions/img/suggestion_code_block_output_v12_8.png b/doc/user/discussions/img/suggestion_code_block_output_v12_8.png new file mode 100644 index 0000000000000000000000000000000000000000..74833253aa091b0992d7027939d9587ebb671e2e GIT binary patch literal 29769 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 [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13086) in GitLab 12.7. diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index 994e12445b7..48d4f1a0a63 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 @@ -40,6 +37,8 @@ module API optional :target_path, type: String, desc: 'Target path' end post do + authenticated_as_admin! + message = BroadcastMessage.create(declared_params(include_missing: false)) if message.persisted? @@ -76,6 +75,8 @@ module API optional :target_path, type: String, desc: 'Target path' end put ':id' do + authenticated_as_admin! + message = find_message if message.update(declared_params(include_missing: false)) @@ -93,6 +94,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/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index c8e96c7c5db..cc9d45dcae4 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -104,14 +104,12 @@ 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 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/feature/gitaly.rb b/lib/feature/gitaly.rb index c225ca56c55..96062ae87bc 100644 --- a/lib/feature/gitaly.rb +++ b/lib/feature/gitaly.rb @@ -9,7 +9,6 @@ class Feature %w[ cache_invalidator inforef_uploadpack_cache - get_tag_messages_go filter_shas_with_signatures_go commit_without_batch_check ].freeze 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/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/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/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/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/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index e598cfc143e..c4ac6a3a3f2 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -21,7 +21,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 @@ -72,6 +74,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..3606e1c5bd9 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 diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb index c749816cf6a..2cf2a81f812 100644 --- a/lib/gitlab/middleware/read_only/controller.rb +++ b/lib/gitlab/middleware/read_only/controller.rb @@ -12,12 +12,12 @@ 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 = { diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb index c96212f27a7..c8719023d40 100644 --- a/lib/gitlab/sidekiq_config.rb +++ b/lib/gitlab/sidekiq_config.rb @@ -6,6 +6,7 @@ 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, @@ -13,11 +14,19 @@ module Gitlab ].compact.freeze # For queues that don't have explicit workers - default and mailers - DummyWorker = Struct.new(:queue) + DummyWorker = Struct.new(:queue, :weight) do + def queue_namespace + nil + end + + def get_weight + weight + end + end DEFAULT_WORKERS = [ - Gitlab::SidekiqConfig::Worker.new(DummyWorker.new('default'), ee: false), - Gitlab::SidekiqConfig::Worker.new(DummyWorker.new('mailers'), ee: false) + Gitlab::SidekiqConfig::Worker.new(DummyWorker.new('default', 1), ee: false), + Gitlab::SidekiqConfig::Worker.new(DummyWorker.new('mailers', 2), ee: false) ].freeze class << self @@ -30,7 +39,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 @@ -65,6 +74,28 @@ module Gitlab Gitlab.ee? && ee_workers != YAML.safe_load(File.read(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 + + def sidekiq_queues_yml_outdated? + # YAML.load is OK here as we control the file contents + config_queues = YAML.load(File.read(SIDEKIQ_QUEUES_PATH))[:queues] # rubocop:disable Security/YAMLLoad + + queues_for_sidekiq_queues_yml != config_queues + end + private def find_workers(root, ee:) diff --git a/lib/gitlab/sidekiq_config/worker.rb b/lib/gitlab/sidekiq_config/worker.rb index 313d9b17b16..ac94bab9a8f 100644 --- a/lib/gitlab/sidekiq_config/worker.rb +++ b/lib/gitlab/sidekiq_config/worker.rb @@ -7,8 +7,9 @@ module Gitlab attr_reader :klass delegate :feature_category_not_owned?, :get_feature_category, - :get_worker_resource_boundary, :latency_sensitive_worker?, - :queue, :worker_has_external_dependencies?, + :get_weight, :get_worker_resource_boundary, + :latency_sensitive_worker?, :queue, :queue_namespace, + :worker_has_external_dependencies?, to: :klass def initialize(klass, ee:) @@ -35,7 +36,7 @@ module Gitlab # Put namespaced queues first def to_sort - [queue.include?(':') ? 0 : 1, queue] + [queue_namespace ? 0 : 1, queue] end # YAML representation @@ -46,6 +47,14 @@ module Gitlab def to_yaml queue 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/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake index 15c6c930386..eb3de195626 100644 --- a/lib/tasks/gitlab/sidekiq.rake +++ b/lib/tasks/gitlab/sidekiq.rake @@ -4,8 +4,13 @@ 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 - def write_yaml(path, object) + 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 @@ -13,17 +18,12 @@ namespace :gitlab do # Do not edit it manually! BANNER - File.write(path, banner + YAML.dump(object)) - end - - desc 'GitLab | Sidekiq | Generate all_queues.yml based on worker definitions' - task generate: :environment do foss_workers, ee_workers = Gitlab::SidekiqConfig.workers_for_all_queues_yml - write_yaml(Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH, foss_workers) + write_yaml(Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH, banner, foss_workers) if Gitlab.ee? - write_yaml(Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH, ee_workers) + write_yaml(Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH, banner, ee_workers) end end @@ -44,5 +44,57 @@ namespace :gitlab do 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/lint.rake b/lib/tasks/lint.rake index 91e7031744a..7a4d09bb6d4 100644 --- a/lib/tasks/lint.rake +++ b/lib/tasks/lint.rake @@ -38,11 +38,13 @@ unless Rails.env.production? ] 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/locale/gitlab.pot b/locale/gitlab.pot index 025d21e1c25..74d7681def3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10419,6 +10419,9 @@ msgstr "" msgid "Is using license seat:" msgstr "" +msgid "Is using seat" +msgstr "" + msgid "IssuableStatus|Closed" msgstr "" diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb index 07dea3449f1..d673efd1970 100644 --- a/qa/qa/page/project/job/show.rb +++ b/qa/qa/page/project/job/show.rb @@ -13,6 +13,10 @@ module QA::Page element :pipeline_path end + view 'app/assets/javascripts/jobs/components/sidebar.vue' do + element :retry_button + end + def successful?(timeout: 60) raise "Timed out waiting for the build trace to load" unless loaded? raise "Timed out waiting for the status to be a valid completed state" unless completed?(timeout: timeout) @@ -33,6 +37,10 @@ module QA::Page result end + def retry! + click_element :retry_button + end + private def loaded?(wait: 60) diff --git a/qa/qa/page/project/settings/ci_variables.rb b/qa/qa/page/project/settings/ci_variables.rb index 64a182e5b3a..2bb285d6086 100644 --- a/qa/qa/page/project/settings/ci_variables.rb +++ b/qa/qa/page/project/settings/ci_variables.rb @@ -52,6 +52,14 @@ module QA end end + def remove_variable(location: :first) + within('.ci-variable-row-body', match: location) do + find('button.ci-variable-row-remove-button').click + end + + save_variables + end + private def toggle_masked(masked_node, masked) diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 0ca49bd080b..19cd7d123c9 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -46,12 +46,7 @@ module QA deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" - Resource::CiVariable.fabricate_via_browser_ui! do |resource| - resource.project = @project - resource.key = deploy_key_name - resource.value = key.private_key - resource.masked = false - end + make_ci_variable(deploy_key_name, key) gitlab_ci = <<~YAML cat-config: @@ -90,6 +85,17 @@ module QA expect(job.output).to include(sha1sum) end end + + private + + def make_ci_variable(key_name, key) + Resource::CiVariable.fabricate_via_api! do |resource| + resource.project = @project + resource.key = key_name + resource.value = key.private_key + resource.masked = false + end + end end end end diff --git a/spec/controllers/concerns/lfs_request_spec.rb b/spec/controllers/concerns/lfs_request_spec.rb index 584448e68f9..79257e9a7f6 100644 --- a/spec/controllers/concerns/lfs_request_spec.rb +++ b/spec/controllers/concerns/lfs_request_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe LfsRequest do include ProjectForksHelper - controller(Projects::GitHttpClientController) do + controller(Repositories::GitHttpClientController) do # `described_class` is not available in this context include LfsRequest diff --git a/spec/controllers/projects/git_http_controller_spec.rb b/spec/controllers/projects/git_http_controller_spec.rb deleted file mode 100644 index 4df53121aaa..00000000000 --- a/spec/controllers/projects/git_http_controller_spec.rb +++ /dev/null @@ -1,107 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Projects::GitHttpController do - include GitHttpHelpers - - let_it_be(:project) { create(:project, :public, :repository) } - let(:project_params) do - { - namespace_id: project.namespace.to_param, - project_id: project.path + '.git' - } - end - let(:params) { project_params } - - describe 'HEAD #info_refs' do - it 'returns 403' do - head :info_refs, params: { namespace_id: project.namespace.to_param, project_id: project.path + '.git' } - - expect(response.status).to eq(403) - end - end - - describe 'GET #info_refs' do - let(:params) { project_params.merge(service: 'git-upload-pack') } - - it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do - stub_application_setting(enabled_git_access_protocol: 'ssh') - - get :info_refs, params: params - - expect(response.status).to eq(401) - end - - context 'with authorized user' do - let(:user) { project.owner } - - before do - request.headers.merge! auth_env(user.username, user.password, nil) - end - - it 'returns 200' do - get :info_refs, params: params - - expect(response.status).to eq(200) - end - - it 'updates the user activity' do - expect_next_instance_of(Users::ActivityService) do |activity_service| - expect(activity_service).to receive(:execute) - end - - get :info_refs, params: params - end - end - - context 'with exceptions' do - before do - allow(controller).to receive(:verify_workhorse_api!).and_return(true) - end - - it 'returns 503 with GRPC Unavailable' do - allow(controller).to receive(:access_check).and_raise(GRPC::Unavailable) - - get :info_refs, params: params - - expect(response.status).to eq(503) - end - - it 'returns 503 with timeout error' do - allow(controller).to receive(:access_check).and_raise(Gitlab::GitAccess::TimeoutError) - - get :info_refs, params: params - - expect(response.status).to eq(503) - expect(response.body).to eq 'Gitlab::GitAccess::TimeoutError' - end - end - end - - describe 'POST #git_upload_pack' do - before do - allow(controller).to receive(:authenticate_user).and_return(true) - allow(controller).to receive(:verify_workhorse_api!).and_return(true) - allow(controller).to receive(:access_check).and_return(nil) - end - - after do - post :git_upload_pack, params: params - end - - context 'on a read-only instance' do - before do - allow(Gitlab::Database).to receive(:read_only?).and_return(true) - end - - it 'does not update project statistics' do - expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async) - end - end - - it 'updates project statistics' do - expect(ProjectDailyStatisticsWorker).to receive(:perform_async) - end - end -end diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb new file mode 100644 index 00000000000..10a7b72ca89 --- /dev/null +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Repositories::GitHttpController do + include GitHttpHelpers + + let_it_be(:project) { create(:project, :public, :repository) } + + let(:namespace_id) { project.namespace.to_param } + let(:repository_id) { project.path + '.git' } + let(:project_params) do + { + namespace_id: namespace_id, + repository_id: repository_id + } + end + let(:params) { project_params } + + describe 'HEAD #info_refs' do + it 'returns 403' do + head :info_refs, params: params + + expect(response.status).to eq(403) + end + end + + shared_examples 'info_refs behavior' do + describe 'GET #info_refs' do + let(:params) { project_params.merge(service: 'git-upload-pack') } + + it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do + stub_application_setting(enabled_git_access_protocol: 'ssh') + allow(controller).to receive(:basic_auth_provided?).and_call_original + + expect(controller).to receive(:http_download_allowed?).and_call_original + + get :info_refs, params: params + + expect(response.status).to eq(401) + end + + context 'with authorized user' do + let(:user) { project.owner } + + before do + request.headers.merge! auth_env(user.username, user.password, nil) + end + + it 'returns 200' do + get :info_refs, params: params + + expect(response.status).to eq(200) + end + + it 'updates the user activity' do + expect_next_instance_of(Users::ActivityService) do |activity_service| + expect(activity_service).to receive(:execute) + end + + get :info_refs, params: params + end + end + + context 'with exceptions' do + before do + allow(controller).to receive(:verify_workhorse_api!).and_return(true) + end + + it 'returns 503 with GRPC Unavailable' do + allow(controller).to receive(:access_check).and_raise(GRPC::Unavailable) + + get :info_refs, params: params + + expect(response.status).to eq(503) + end + + it 'returns 503 with timeout error' do + allow(controller).to receive(:access_check).and_raise(Gitlab::GitAccess::TimeoutError) + + get :info_refs, params: params + + expect(response.status).to eq(503) + expect(response.body).to eq 'Gitlab::GitAccess::TimeoutError' + end + end + end + end + + shared_examples 'git_upload_pack behavior' do |expected| + describe 'POST #git_upload_pack' do + before do + allow(controller).to receive(:authenticate_user).and_return(true) + allow(controller).to receive(:verify_workhorse_api!).and_return(true) + allow(controller).to receive(:access_check).and_return(nil) + end + + after do + post :git_upload_pack, params: params + end + + context 'on a read-only instance' do + before do + allow(Gitlab::Database).to receive(:read_only?).and_return(true) + end + + it 'does not update project statistics' do + expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async) + end + end + + if expected + it 'updates project statistics' do + expect(ProjectDailyStatisticsWorker).to receive(:perform_async) + end + else + it 'does not update project statistics' do + expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async) + end + end + end + end + + shared_examples 'access checker class' do + let(:params) { project_params.merge(service: 'git-upload-pack') } + + it 'calls the right access class checker with the right object' do + allow(controller).to receive(:verify_workhorse_api!).and_return(true) + + access_double = double + expect(expected_class).to receive(:new).with(anything, expected_object, 'http', anything).and_return(access_double) + allow(access_double).to receive(:check).and_return(false) + + get :info_refs, params: params + end + end + + context 'when repository container is a project' do + it_behaves_like 'info_refs behavior' + it_behaves_like 'git_upload_pack behavior', true + it_behaves_like 'access checker class' do + let(:expected_class) { Gitlab::GitAccess } + let(:expected_object) { project } + end + end +end diff --git a/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb b/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb index 30cede6f4cf..3a512fee3b3 100644 --- a/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb +++ b/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb @@ -20,6 +20,7 @@ describe GitlabSchema.types['SentryDetailedError'] do message culprit externalUrl + externalBaseUrl sentryProjectId sentryProjectName sentryProjectSlug @@ -30,8 +31,10 @@ describe GitlabSchema.types['SentryDetailedError'] do lastReleaseLastCommit firstReleaseShortVersion lastReleaseShortVersion + gitlabIssuePath gitlabCommit gitlabCommitPath + tags ] is_expected.to have_graphql_fields(*expected_fields) diff --git a/spec/lib/gitaly/server_spec.rb b/spec/lib/gitaly/server_spec.rb index 184d049d1fb..5142f705251 100644 --- a/spec/lib/gitaly/server_spec.rb +++ b/spec/lib/gitaly/server_spec.rb @@ -66,6 +66,53 @@ describe Gitaly::Server do end end + context "when examining disk statistics for a given server" do + let(:disk_available) { 42 } + let(:disk_used) { 42 } + let(:storage_status) { double('storage_status') } + + before do + allow(storage_status).to receive(:storage_name).and_return('default') + allow(storage_status).to receive(:available).and_return(disk_available) + allow(storage_status).to receive(:used).and_return(disk_used) + response = double("response") + allow(response).to receive(:storage_statuses).and_return([storage_status]) + allow_next_instance_of(Gitlab::GitalyClient::ServerService) do |instance| + allow(instance).to receive(:disk_statistics).and_return(response) + end + end + + describe '#disk_available' do + subject { server.disk_available } + + it { is_expected.to be_present } + + it "returns disk available for the storage of the instantiated server" do + is_expected.to eq(disk_available) + end + end + + describe '#disk_used' do + subject { server.disk_used } + + it { is_expected.to be_present } + + it "returns disk used for the storage of the instantiated server" do + is_expected.to eq(disk_used) + end + end + + describe '#disk_stats' do + subject { server.disk_stats } + + it { is_expected.to be_present } + + it "returns the storage of the instantiated server" do + is_expected.to eq(storage_status) + end + end + end + describe '#expected_version?' do using RSpec::Parameterized::TableSyntax diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index ebf56c0ae66..b03c1feb429 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -52,7 +52,7 @@ describe Gitlab::GitalyClient do end describe '.filesystem_id' do - it 'returns an empty string when the storage is not found in the response' do + it 'returns an empty string when the relevant storage status is not found in the response' do response = double("response") allow(response).to receive(:storage_statuses).and_return([]) allow_next_instance_of(Gitlab::GitalyClient::ServerService) do |instance| @@ -63,6 +63,63 @@ describe Gitlab::GitalyClient do end end + context 'when the relevant storage status is not found' do + before do + response = double('response') + allow(response).to receive(:storage_statuses).and_return([]) + allow_next_instance_of(Gitlab::GitalyClient::ServerService) do |instance| + allow(instance).to receive(:disk_statistics).and_return(response) + expect(instance).to receive(:storage_disk_statistics) + end + end + + describe '.filesystem_disk_available' do + it 'returns nil when the relevant storage status is not found in the response' do + expect(described_class.filesystem_disk_available('default')).to eq(nil) + end + end + + describe '.filesystem_disk_used' do + it 'returns nil when the relevant storage status is not found in the response' do + expect(described_class.filesystem_disk_used('default')).to eq(nil) + end + end + end + + context 'when the relevant storage status is found' do + let(:disk_available) { 42 } + let(:disk_used) { 42 } + let(:storage_status) { double('storage_status') } + + before do + allow(storage_status).to receive(:storage_name).and_return('default') + allow(storage_status).to receive(:used).and_return(disk_used) + allow(storage_status).to receive(:available).and_return(disk_available) + response = double('response') + allow(response).to receive(:storage_statuses).and_return([storage_status]) + allow_next_instance_of(Gitlab::GitalyClient::ServerService) do |instance| + allow(instance).to receive(:disk_statistics).and_return(response) + end + expect_next_instance_of(Gitlab::GitalyClient::ServerService) do |instance| + expect(instance).to receive(:storage_disk_statistics).and_return(storage_status) + end + end + + describe '.filesystem_disk_available' do + it 'returns disk available when the relevant storage status is found in the response' do + expect(storage_status).to receive(:available) + expect(described_class.filesystem_disk_available('default')).to eq(disk_available) + end + end + + describe '.filesystem_disk_used' do + it 'returns disk used when the relevant storage status is found in the response' do + expect(storage_status).to receive(:used) + expect(described_class.filesystem_disk_used('default')).to eq(disk_used) + end + end + end + describe '.stub_class' do it 'returns the gRPC health check stub' do expect(described_class.stub_class(:health_check)).to eq(::Grpc::Health::V1::Health::Stub) diff --git a/spec/lib/gitlab/import_export/import_failure_service_spec.rb b/spec/lib/gitlab/import_export/import_failure_service_spec.rb index 0351f88afdb..324328181e4 100644 --- a/spec/lib/gitlab/import_export/import_failure_service_spec.rb +++ b/spec/lib/gitlab/import_export/import_failure_service_spec.rb @@ -6,6 +6,7 @@ describe Gitlab::ImportExport::ImportFailureService do let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') } let(:label) { create(:label) } let(:subject) { described_class.new(importable) } + let(:action) { "save_relation" } let(:relation_key) { "labels" } let(:relation_index) { 0 } @@ -15,7 +16,12 @@ describe Gitlab::ImportExport::ImportFailureService do let(:correlation_id) { 'my-correlation-id' } let(:retry_count) { 2 } let(:log_import_failure) do - subject.log_import_failure(relation_key, relation_index, exception, retry_count) + subject.log_import_failure( + source: action, + relation_key: relation_key, + relation_index: relation_index, + exception: exception, + retry_count: retry_count) end before do @@ -44,7 +50,7 @@ describe Gitlab::ImportExport::ImportFailureService do describe '#with_retry' do let(:perform_retry) do - subject.with_retry(relation_key, relation_index) do + subject.with_retry(action: action, relation_key: relation_key, relation_index: relation_index) do label.save! end end @@ -60,7 +66,12 @@ describe Gitlab::ImportExport::ImportFailureService do end it 'retries and logs import failure once with correct params' do - expect(subject).to receive(:log_import_failure).with(relation_key, relation_index, instance_of(exception), 1).once + expect(subject).to receive(:log_import_failure).with( + source: action, + relation_key: relation_key, + relation_index: relation_index, + exception: instance_of(exception), + retry_count: 1).once perform_retry end @@ -85,7 +96,11 @@ describe Gitlab::ImportExport::ImportFailureService do maximum_retry_count.times do |index| retry_count = index + 1 - expect(subject).to receive(:log_import_failure).with(relation_key, relation_index, instance_of(exception), retry_count) + expect(subject).to receive(:log_import_failure).with( + source: action, relation_key: relation_key, + relation_index: relation_index, + exception: instance_of(exception), + retry_count: retry_count) end expect { perform_retry }.to raise_exception(exception) diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index ac9a63e8414..25f70420cda 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -498,6 +498,58 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end end + context 'when post import action throw non-retriable exception' do + let(:exception) { StandardError.new('post_import_error') } + + before do + setup_import_export_config('light') + expect(project) + .to receive(:merge_requests) + .and_raise(exception) + end + + it 'report post import error' do + expect(restored_project_json).to eq(false) + expect(shared.errors).to include('post_import_error') + end + end + + context 'when post import action throw retriable exception one time' do + let(:exception) { GRPC::DeadlineExceeded.new } + + before do + setup_import_export_config('light') + expect(project) + .to receive(:merge_requests) + .and_raise(exception) + expect(project) + .to receive(:merge_requests) + .and_call_original + expect(restored_project_json).to eq(true) + end + + it_behaves_like 'restores project successfully', + issues: 1, + labels: 2, + label_with_priorities: 'A project label', + milestones: 1, + first_issue_labels: 1, + services: 1, + import_failures: 1 + + it 'records the failures in the database' do + import_failure = ImportFailure.last + + expect(import_failure.project_id).to eq(project.id) + expect(import_failure.relation_key).to be_nil + expect(import_failure.relation_index).to be_nil + expect(import_failure.exception_class).to eq('GRPC::DeadlineExceeded') + expect(import_failure.exception_message).to be_present + expect(import_failure.correlation_id_value).not_to be_empty + expect(import_failure.created_at).to be_present + end + end + context 'when the project has overridden params in import data' do before do setup_import_export_config('light') diff --git a/spec/lib/gitlab/sidekiq_config/worker_spec.rb b/spec/lib/gitlab/sidekiq_config/worker_spec.rb index f2fe51abd5e..ba6760f38b5 100644 --- a/spec/lib/gitlab/sidekiq_config/worker_spec.rb +++ b/spec/lib/gitlab/sidekiq_config/worker_spec.rb @@ -3,8 +3,11 @@ require 'fast_spec_helper' describe Gitlab::SidekiqConfig::Worker do - def worker_with_queue(queue) - described_class.new(double(queue: queue), ee: false) + def create_worker(queue:, weight: 0) + namespace = queue.include?(':') && queue.split(':').first + inner_worker = double(queue: queue, queue_namespace: namespace, get_weight: weight) + + described_class.new(inner_worker, ee: false) end describe '#ee?' do @@ -34,9 +37,9 @@ describe Gitlab::SidekiqConfig::Worker do describe 'delegations' do [ - :feature_category_not_owned?, :get_feature_category, + :feature_category_not_owned?, :get_feature_category, :get_weight, :get_worker_resource_boundary, :latency_sensitive_worker?, :queue, - :worker_has_external_dependencies? + :queue_namespace, :worker_has_external_dependencies? ].each do |meth| it "delegates #{meth} to the worker class" do worker = double @@ -50,8 +53,8 @@ describe Gitlab::SidekiqConfig::Worker do describe 'sorting' do it 'sorts queues with a namespace before those without a namespace' do - namespaced_worker = worker_with_queue('namespace:queue') - plain_worker = worker_with_queue('a_queue') + namespaced_worker = create_worker(queue: 'namespace:queue') + plain_worker = create_worker(queue: 'a_queue') expect([plain_worker, namespaced_worker].sort) .to eq([namespaced_worker, plain_worker]) @@ -59,12 +62,12 @@ describe Gitlab::SidekiqConfig::Worker do it 'sorts alphabetically by queue' do workers = [ - worker_with_queue('namespace:a'), - worker_with_queue('namespace:b'), - worker_with_queue('other_namespace:a'), - worker_with_queue('other_namespace:b'), - worker_with_queue('a'), - worker_with_queue('b') + create_worker(queue: 'namespace:a'), + create_worker(queue: 'namespace:b'), + create_worker(queue: 'other_namespace:a'), + create_worker(queue: 'other_namespace:b'), + create_worker(queue: 'a'), + create_worker(queue: 'b') ] expect(workers.shuffle.sort).to eq(workers) @@ -73,12 +76,26 @@ describe Gitlab::SidekiqConfig::Worker do describe 'YAML encoding' do it 'encodes the worker in YAML as a string of the queue' do - worker_a = worker_with_queue('a') - worker_b = worker_with_queue('b') + worker_a = create_worker(queue: 'a') + worker_b = create_worker(queue: 'b') expect(YAML.dump(worker_a)).to eq(YAML.dump('a')) expect(YAML.dump([worker_a, worker_b])) .to eq(YAML.dump(%w[a b])) end end + + describe '#namespace_and_weight' do + it 'returns a namespace, weight pair for the worker' do + expect(create_worker(queue: 'namespace:a', weight: 2).namespace_and_weight) + .to eq(['namespace', 2]) + end + end + + describe '#queue_and_weight' do + it 'returns a queue, weight pair for the worker' do + expect(create_worker(queue: 'namespace:a', weight: 2).queue_and_weight) + .to eq(['namespace:a', 2]) + end + end end diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb index 39bb149cf73..20690a35dc8 100644 --- a/spec/lib/gitlab/sidekiq_config_spec.rb +++ b/spec/lib/gitlab/sidekiq_config_spec.rb @@ -80,4 +80,64 @@ describe Gitlab::SidekiqConfig do expect(described_class.all_queues_yml_outdated?).to be(false) end end + + describe '.queues_for_sidekiq_queues_yml' do + before do + workers = [ + Namespaces::RootStatisticsWorker, + Namespaces::ScheduleAggregationWorker, + MergeWorker, + ProcessCommitWorker + ].map { |worker| described_class::Worker.new(worker, ee: false) } + + allow(described_class).to receive(:workers).and_return(workers) + end + + it 'returns queues and weights, aggregating namespaces with the same weight' do + expected_queues = [ + ['merge', 5], + ['process_commit', 3], + ['update_namespace_statistics', 1] + ] + + expect(described_class.queues_for_sidekiq_queues_yml).to eq(expected_queues) + end + end + + describe '.sidekiq_queues_yml_outdated?' do + before do + workers = [ + Namespaces::RootStatisticsWorker, + Namespaces::ScheduleAggregationWorker, + MergeWorker, + ProcessCommitWorker + ].map { |worker| described_class::Worker.new(worker, ee: false) } + + allow(described_class).to receive(:workers).and_return(workers) + end + + let(:expected_queues) do + [ + ['merge', 5], + ['process_commit', 3], + ['update_namespace_statistics', 1] + ] + end + + it 'returns true if the YAML file does not match the application code' do + allow(File).to receive(:read) + .with(described_class::SIDEKIQ_QUEUES_PATH) + .and_return(YAML.dump(queues: expected_queues.reverse)) + + expect(described_class.sidekiq_queues_yml_outdated?).to be(true) + end + + it 'returns false if the YAML file matches the application code' do + allow(File).to receive(:read) + .with(described_class::SIDEKIQ_QUEUES_PATH) + .and_return(YAML.dump(queues: expected_queues)) + + expect(described_class.sidekiq_queues_yml_outdated?).to be(false) + end + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 38e15fc4582..4f729f2546c 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2391,6 +2391,8 @@ describe Ci::Build do { key: 'GITLAB_CI', value: 'true', public: true, masked: false }, { key: 'CI_SERVER_URL', value: Gitlab.config.gitlab.url, public: true, masked: false }, { key: 'CI_SERVER_HOST', value: Gitlab.config.gitlab.host, public: true, masked: false }, + { key: 'CI_SERVER_PORT', value: Gitlab.config.gitlab.port.to_s, public: true, masked: false }, + { key: 'CI_SERVER_PROTOCOL', value: Gitlab.config.gitlab.protocol, public: true, masked: false }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true, masked: false }, { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true, masked: false }, { key: 'CI_SERVER_VERSION_MAJOR', value: Gitlab.version_info.major.to_s, public: true, masked: false }, diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb index 9dc639a25a2..a502d597da1 100644 --- a/spec/requests/api/broadcast_messages_spec.rb +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -8,22 +8,10 @@ describe API::BroadcastMessages do set(:message) { create(:broadcast_message) } describe 'GET /broadcast_messages' do - it 'returns a 401 for anonymous users' do - get api('/broadcast_messages') - - expect(response).to have_gitlab_http_status(401) - end - - it 'returns a 403 for users' do - get api('/broadcast_messages', user) - - expect(response).to have_gitlab_http_status(403) - end - - it 'returns an Array of BroadcastMessages for admins' do + it 'returns an Array of BroadcastMessages' do create(:broadcast_message) - get api('/broadcast_messages', admin) + get api('/broadcast_messages') expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers @@ -34,21 +22,9 @@ describe API::BroadcastMessages do end describe 'GET /broadcast_messages/:id' do - it 'returns a 401 for anonymous users' do + it 'returns the specified message' do get api("/broadcast_messages/#{message.id}") - expect(response).to have_gitlab_http_status(401) - end - - it 'returns a 403 for users' do - get api("/broadcast_messages/#{message.id}", user) - - expect(response).to have_gitlab_http_status(403) - end - - it 'returns the specified message for admins' do - get api("/broadcast_messages/#{message.id}", admin) - expect(response).to have_gitlab_http_status(200) expect(json_response['id']).to eq message.id expect(json_response.keys) diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb index 664206dec29..a1f9fa1f10c 100644 --- a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb +++ b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb @@ -57,6 +57,10 @@ describe 'getting a detailed sentry error' do expect(error_data['firstSeen']).to eql sentry_detailed_error.first_seen expect(error_data['lastSeen']).to eql sentry_detailed_error.last_seen expect(error_data['gitlabCommit']).to be nil + expect(error_data['externalBaseUrl']).to eq sentry_detailed_error.external_base_url + expect(error_data['gitlabIssuePath']).to eq sentry_detailed_error.gitlab_issue + expect(error_data['tags']['logger']).to eq sentry_detailed_error.tags[:logger] + expect(error_data['tags']['level']).to eq sentry_detailed_error.tags[:level] end it 'is expected to return the frequency correctly' do diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 50754773fad..6f784830f9d 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -326,7 +326,7 @@ describe API::Internal::Base do expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) - expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-get-tag-messages-go' => 'true', 'gitaly-feature-filter-shas-with-signatures-go' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true') + expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-filter-shas-with-signatures-go' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true') expect(user.reload.last_activity_on).to eql(Date.today) end end @@ -346,7 +346,7 @@ describe API::Internal::Base do expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) - expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-get-tag-messages-go' => 'true', 'gitaly-feature-filter-shas-with-signatures-go' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true') + expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-filter-shas-with-signatures-go' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true') expect(user.reload.last_activity_on).to be_nil end end @@ -594,7 +594,7 @@ describe API::Internal::Base do expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) - expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-get-tag-messages-go' => 'true', 'gitaly-feature-filter-shas-with-signatures-go' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true') + expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-filter-shas-with-signatures-go' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true') end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 42b4bd71b88..c3a5c0b0caa 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -108,7 +108,7 @@ describe 'Git HTTP requests' do shared_examples_for 'project path without .git suffix' do context "GET info/refs" do - let(:path) { "/#{project_path}/info/refs" } + let(:path) { "/#{repository_path}/info/refs" } context "when no params are added" do before do @@ -116,7 +116,7 @@ describe 'Git HTTP requests' do end it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project_path}.git/info/refs") + expect(response).to redirect_to("/#{repository_path}.git/info/refs") end end @@ -128,7 +128,7 @@ describe 'Git HTTP requests' do end it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}") + expect(response).to redirect_to("/#{repository_path}.git/info/refs?service=#{params[:service]}") end end @@ -140,7 +140,7 @@ describe 'Git HTTP requests' do end it "redirects to the .git suffix version" do - expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}") + expect(response).to redirect_to("/#{repository_path}.git/info/refs?service=#{params[:service]}") end end @@ -159,13 +159,13 @@ describe 'Git HTTP requests' do context "POST git-upload-pack" do it "fails to find a route" do - expect { clone_post(project_path) }.to raise_error(ActionController::RoutingError) + expect { clone_post(repository_path) }.to raise_error(ActionController::RoutingError) end end context "POST git-receive-pack" do it "fails to find a route" do - expect { push_post(project_path) }.to raise_error(ActionController::RoutingError) + expect { push_post(repository_path) }.to raise_error(ActionController::RoutingError) end end end @@ -211,7 +211,7 @@ describe 'Git HTTP requests' do end it_behaves_like 'project path without .git suffix' do - let(:project_path) { "#{user.namespace.path}/project.git-project" } + let(:repository_path) { "#{user.namespace.path}/project.git-project" } end end end @@ -820,7 +820,7 @@ describe 'Git HTTP requests' do end it_behaves_like 'project path without .git suffix' do - let(:project_path) { create(:project, :repository, :public, path: 'project.git-project').full_path } + let(:repository_path) { create(:project, :repository, :public, path: 'project.git-project').full_path } end context "retrieving an info/refs file" do diff --git a/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb index 55bd2401db1..801be5ae946 100644 --- a/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb @@ -3,6 +3,7 @@ RSpec.shared_examples 'log import failure' do |importable_column| it 'tracks error' do extra = { + source: action, relation_key: relation_key, relation_index: relation_index, retry_count: retry_count @@ -11,7 +12,12 @@ RSpec.shared_examples 'log import failure' do |importable_column| expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception, extra) - subject.log_import_failure(relation_key, relation_index, exception, retry_count) + subject.log_import_failure( + source: action, + relation_key: relation_key, + relation_index: relation_index, + exception: exception, + retry_count: retry_count) end it 'saves data to ImportFailure' do @@ -21,6 +27,7 @@ RSpec.shared_examples 'log import failure' do |importable_column| aggregate_failures do expect(import_failure[importable_column]).to eq(importable.id) + expect(import_failure.source).to eq(action) expect(import_failure.relation_key).to eq(relation_key) expect(import_failure.relation_index).to eq(relation_index) expect(import_failure.exception_class).to eq('StandardError')