diff options
160 files changed, 1736 insertions, 962 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 9e49144e11e..bab51568cac 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -63,10 +63,17 @@ GITALY_SERVER_VERSION @project_278964_bot6 @gitlab-org/maintainers/rails-backend # This entry must occur before `/scripts/` in order to be matched first /scripts/remote_development/ -[Engineering Productivity] @gl-quality/eng-prod +# This entry must occur before `/scripts/glfm/**/*`/`/scripts/lib/glfm/**/*` in order to be matched first +/scripts/glfm/**/*.js @gitlab-org/maintainers/frontend +/scripts/lib/glfm/**/*.js @gitlab-org/maintainers/frontend +/scripts/glfm/**/* @gitlab-org/maintainers/rails-backend +/scripts/lib/glfm/**/* @gitlab-org/maintainers/rails-backend + +[Pipeline configuration] @gl-quality/eng-prod /.gitlab-ci.yml /.gitlab/ci/ /.gitlab/ci/docs.gitlab-ci.yml @gl-quality/eng-prod @gl-docsteam +/.gitlab/ci/frontend.gitlab-ci.yml @gl-quality/eng-prod @gitlab-org/maintainers/frontend /.gitlab/ci/package-and-test/ @gl-quality/eng-prod @gl-quality/qe-maintainers /.gitlab/ci/qa.gitlab-ci.yml @gl-quality/eng-prod @gl-quality/qe-maintainers /.gitlab/ci/qa-common/ @gl-quality/eng-prod @gl-quality/qe-maintainers @@ -74,14 +81,29 @@ GITALY_SERVER_VERSION @project_278964_bot6 @gitlab-org/maintainers/rails-backend /.gitlab/ci/reports.gitlab-ci.yml @gl-quality/eng-prod @gitlab-com/gl-security/appsec /.gitlab/ci/review-apps/qa.gitlab-ci.yml @gl-quality/eng-prod @gl-quality/qe-maintainers /.gitlab/ci/test-on-gdk/ @gl-quality/eng-prod @gl-quality/qe-maintainers +/gems/gem.gitlab-ci.yml + +[Tooling] @gl-quality/eng-prod +/.gitlab/CODEOWNERS Dangerfile /danger/ -/gems/gem.gitlab-ci.yml /tooling/danger/ /scripts/ +/scripts/**/*.rb @gl-quality/eng-prod @gitlab-org/maintainers/rails-backend +/scripts/**/*.js @gl-quality/eng-prod @gitlab-org/maintainers/frontend /scripts/frontend/ @gl-quality/eng-prod @gitlab-org/maintainers/frontend /scripts/review_apps/seed-dast-test-data.sh @gl-quality/eng-prod @dappelt @ngeorge1 -.editorconfig +/.codeclimate.yml +/.dockerignore +/.editorconfig +/.gitpod.yml +/.haml-lint_todo.yml +/.haml-lint.yml +/.nvmrc +/.ruby-version +/.tool-versions +/lefthook.yml +/tests.yml ^[Backend Static Code Analysis] @gl-quality/eng-prod @dstull @splattael .rubocop*.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7259015e0b0..0ee1d5da0cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -760,6 +760,10 @@ No changes. - [Add schema_version in the commits index mapping](gitlab-org/gitlab@e75b94903b69e1e1588e251217926882875555a8) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123435)) **GitLab Enterprise Edition** - [Allow to set labels for Redis calls](gitlab-org/gitlab@8ccfff9e2d250eb22afaa7d0243e707b536a5436) ([merge request](gitlab-org/gitlab!122340)) +## 16.1.4 (2023-08-03) + +No changes. + ## 16.1.3 (2023-08-01) ### Added (1 change) @@ -404,7 +404,7 @@ group :development, :test do gem 'parser', '~> 3.2', '>= 3.2.2.3' gem 'pry-byebug' gem 'pry-rails', '~> 0.3.9' - gem 'pry-shell', '~> 0.6.1' + gem 'pry-shell', '~> 0.6.4' gem 'awesome_print', require: false diff --git a/Gemfile.checksum b/Gemfile.checksum index bb8ca77aac3..9ca019df145 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -461,7 +461,7 @@ {"name":"pry","version":"0.14.2","platform":"ruby","checksum":"c4fe54efedaca1d351280b45b8849af363184696fcac1c72e0415f9bdac4334d"}, {"name":"pry-byebug","version":"3.10.1","platform":"ruby","checksum":"c8f975c32255bfdb29e151f5532130be64ff3d0042dc858d0907e849125581f8"}, {"name":"pry-rails","version":"0.3.9","platform":"ruby","checksum":"468662575abb6b67f4a9831219f99290d5eae7bf186e64dd810d0a3e4a8cc4b1"}, -{"name":"pry-shell","version":"0.6.1","platform":"ruby","checksum":"a99a6b3dffe4df274ea1751866816906861a23851f13346e10a8e8f61b53360c"}, +{"name":"pry-shell","version":"0.6.4","platform":"ruby","checksum":"ad024882d29912b071a7de65ebea538b242d2dc1498c60c7c2352ef94769f208"}, {"name":"public_suffix","version":"5.0.0","platform":"ruby","checksum":"26ee4fbce33ada25eb117ac71f2c24bf4d8b3414ab6b34f05b4708a3e90f1c6b"}, {"name":"puma","version":"6.3.0","platform":"java","checksum":"5e2ff95953608d1ba0350b80a3961a43e9bbb78ec60ebd5e4db1940c2921d5d8"}, {"name":"puma","version":"6.3.0","platform":"ruby","checksum":"b0e35b4fe7ae440237a9ff1647c6bb252a1c0951ff356020670d2e62c1aeeeec"}, diff --git a/Gemfile.lock b/Gemfile.lock index 86e45f1b740..a321f3be7fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1217,7 +1217,7 @@ GEM pry (>= 0.13, < 0.15) pry-rails (0.3.9) pry (>= 0.10.4) - pry-shell (0.6.1) + pry-shell (0.6.4) pry (>= 0.13.0) tty-markdown tty-prompt @@ -1941,7 +1941,7 @@ DEPENDENCIES prometheus-client-mmap (~> 0.27) pry-byebug pry-rails (~> 0.3.9) - pry-shell (~> 0.6.1) + pry-shell (~> 0.6.4) puma (~> 6.3) rack (~> 2.2.7) rack-attack (~> 6.6.1) diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index f80f7896790..08c98298121 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -687,11 +687,23 @@ export function redirectTo(url) { } /** - * Navigates to a URL - * @param {*} url - url to navigate to + * Navigates to a URL. + * + * If destination is a querystring, it will be automatically transformed into a fully qualified URL. + * If the URL is not a safe URL (see isSafeURL implementation), this function will log an exception into Sentry. + * + * @param {*} destination - url to navigate to. This can be a fully qualified URL or a querystring. * @param {*} external - if true, open a new page or tab */ -export function visitUrl(url, external = false) { +export function visitUrl(destination, external = false) { + let url = destination; + + if (destination.startsWith('?')) { + const currentUrl = new URL(window.location.href); + currentUrl.search = destination; + url = currentUrl.toString(); + } + if (!isSafeURL(url)) { // For now log this to Sentry and do not block the execution. // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121551#note_1408873600 diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 1bb44988c4d..0444eca9aa7 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import Visibility from 'visibilityjs'; import Vue from 'vue'; +import actionCable from '~/actioncable_consumer'; import Api from '~/api'; import { createAlert, VARIANT_INFO } from '~/alert'; import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants'; @@ -151,7 +152,30 @@ export const initPolling = ({ state, dispatch, getters, commit }) => { dispatch('setLastFetchedAt', getters.getNotesDataByProp('lastFetchedAt')); - dispatch('poll'); + if (gon.features?.actionCableNotes) { + actionCable.subscriptions.create( + { + channel: 'Noteable::NotesChannel', + project_id: state.notesData.projectId, + group_id: state.notesData.groupId, + noteable_type: state.notesData.noteableType, + noteable_id: state.notesData.noteableId, + }, + { + connected() { + dispatch('fetchUpdatedNotes'); + }, + received(data) { + if (data.event === 'updated') { + dispatch('fetchUpdatedNotes'); + } + }, + }, + ); + } else { + dispatch('poll'); + } + commit(types.SET_IS_POLLING_INITIALIZED, true); }; @@ -491,7 +515,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { {"commands_changes":{},"valid":false,"errors":{"commands_only":["Commands applied"]}} */ if (hasQuickActions && message) { - eTagPoll.makeRequest(); + if (eTagPoll) eTagPoll.makeRequest(); // synchronizing the quick action with the sidebar widget // this is a temporary solution until we have confidentiality real-time updates @@ -592,6 +616,21 @@ const getFetchDataParams = (state) => { return { endpoint, options }; }; +export const fetchUpdatedNotes = ({ commit, state, getters, dispatch }) => { + const { endpoint, options } = getFetchDataParams(state); + + return axios + .get(endpoint, options) + .then(({ data }) => { + pollSuccessCallBack(data, commit, state, getters, dispatch); + }) + .catch(() => { + createAlert({ + message: __('Something went wrong while fetching latest comments.'), + }); + }); +}; + export const poll = ({ commit, state, getters, dispatch }) => { const notePollOccurrenceTracking = create(); let alert; diff --git a/app/channels/noteable/notes_channel.rb b/app/channels/noteable/notes_channel.rb new file mode 100644 index 00000000000..021bc3ccd1b --- /dev/null +++ b/app/channels/noteable/notes_channel.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Noteable + class NotesChannel < ApplicationCable::Channel + def subscribed + project = Project.find(params[:project_id]) if params[:project_id].present? + + noteable = NotesFinder.new(current_user, { + project: project, + group_id: params[:group_id], + target_type: params[:noteable_type], + target_id: params[:noteable_id] + }).target + + return reject if noteable.nil? + return reject if Feature.disabled?(:action_cable_notes, project || noteable.try(:group)) + + stream_for noteable + rescue ActiveRecord::RecordNotFound + reject + end + end +end diff --git a/app/controllers/concerns/web_ide_csp.rb b/app/controllers/concerns/web_ide_csp.rb index 0327020a0c2..90d6ff38e90 100644 --- a/app/controllers/concerns/web_ide_csp.rb +++ b/app/controllers/concerns/web_ide_csp.rb @@ -21,7 +21,7 @@ module WebIdeCSP default_src = Array(request.content_security_policy.directives['default-src'] || []) request.content_security_policy.directives['frame-src'] ||= default_src - request.content_security_policy.directives['frame-src'].concat([webpack_url, 'https://*.vscode-cdn.net/']) + request.content_security_policy.directives['frame-src'].concat([webpack_url, 'https://*.web-ide.gitlab-static.net/']) request.content_security_policy.directives['worker-src'] ||= default_src request.content_security_policy.directives['worker-src'].concat([webpack_url]) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 44e55147181..5123faa6c47 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -72,6 +72,7 @@ class Projects::IssuesController < Projects::ApplicationController push_frontend_feature_flag(:epic_widget_edit_confirmation, project) push_frontend_feature_flag(:moved_mr_sidebar, project) push_frontend_feature_flag(:move_close_into_dropdown, project) + push_frontend_feature_flag(:action_cable_notes, project) end around_action :allow_gitaly_ref_name_caching, only: [:discussions] diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 2172c91fc76..9ad40ea622b 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -50,6 +50,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:mr_activity_filters, current_user) push_frontend_feature_flag(:review_apps_redeploy_mr_widget, project) push_frontend_feature_flag(:ci_job_failures_in_mr, project) + push_frontend_feature_flag(:action_cable_notes, project) end before_action only: [:edit] do diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 8f972f207a4..4eb0f59175e 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -318,6 +318,7 @@ module ApplicationSettingsHelper :max_export_size, :max_import_size, :max_import_remote_file_size, + :max_decompressed_archive_size, :max_pages_size, :max_pages_custom_domains_per_project, :max_terraform_state_size_bytes, diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 3e8872dc199..af8da86b391 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -178,6 +178,10 @@ module NotesHelper def notes_data(issuable) data = { + noteableType: @noteable.class.underscore, + noteableId: @noteable.id, + projectId: @project&.id, + groupId: @group&.id, discussionsPath: discussions_path(issuable), registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'), diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index ea9bfe3253f..6a11aeeadb3 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -8,6 +8,7 @@ class DeviseMailer < Devise::Mailer helper EmailsHelper helper ApplicationHelper + helper RegistrationsHelper def password_change_by_admin(record, opts = {}) devise_mail(record, :password_change_by_admin, opts) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 036a0fc012e..4180e76e1a0 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -39,6 +39,7 @@ class Notify < ApplicationMailer helper GitlabRoutingHelper helper IssuablesHelper helper InProductMarketingHelper + helper RegistrationsHelper def test_email(recipient_email, subject, body) mail_with_locale( diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 44c6e4d949f..9f3832178aa 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -263,6 +263,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :max_decompressed_archive_size, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :max_pages_size, presence: true, numericality: { diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index f2b11e69737..42c7ce44dca 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -120,6 +120,7 @@ module ApplicationSettingImplementation max_export_size: 0, max_import_size: 0, max_import_remote_file_size: 10240, + max_decompressed_archive_size: 25600, max_terraform_state_size_bytes: 0, max_yaml_size_bytes: 1.megabyte, max_yaml_depth: 100, diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 5c91f2460c4..b30d60652c2 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -171,9 +171,9 @@ module Noteable return unless etag_caching_enabled? # TODO: We need to figure out a way to make ETag caching work for group-level work items - return if is_a?(Issue) && project.nil? + Gitlab::EtagCaching::Store.new.touch(note_etag_key) unless is_a?(Issue) && project.nil? - Gitlab::EtagCaching::Store.new.touch(note_etag_key) + Noteable::NotesChannel.broadcast_to(self, event: 'updated') if Feature.enabled?(:action_cable_notes, project || try(:group)) end def note_etag_key diff --git a/app/models/concerns/web_hooks/auto_disabling.rb b/app/models/concerns/web_hooks/auto_disabling.rb index d6c900f75a9..72812f35f72 100644 --- a/app/models/concerns/web_hooks/auto_disabling.rb +++ b/app/models/concerns/web_hooks/auto_disabling.rb @@ -3,6 +3,7 @@ module WebHooks module AutoDisabling extend ActiveSupport::Concern + include ::Gitlab::Loggable ENABLED_HOOK_TYPES = %w[ProjectHook].freeze MAX_FAILURES = 100 @@ -86,17 +87,14 @@ module WebHooks recent_failures > FAILURE_THRESHOLD && disabled_until.blank? end - def disable! - return if !auto_disabling_enabled? || permanently_disabled? - - update_attribute(:recent_failures, EXCEEDED_FAILURE_THRESHOLD) - end - def enable! return unless auto_disabling_enabled? return if recent_failures == 0 && disabled_until.nil? && backoff_count == 0 - assign_attributes(recent_failures: 0, disabled_until: nil, backoff_count: 0) + attrs = { recent_failures: 0, disabled_until: nil, backoff_count: 0 } + + assign_attributes(attrs) + logger.info(hook_id: id, action: 'enable', **attrs) save(validate: false) end @@ -114,14 +112,21 @@ module WebHooks end assign_attributes(attrs) - save(validate: false) if changed? + + return unless changed? + + logger.info(hook_id: id, action: 'backoff', **attrs) + save(validate: false) end def failed! return unless auto_disabling_enabled? return unless recent_failures < MAX_FAILURES - assign_attributes(disabled_until: nil, backoff_count: 0, recent_failures: next_failure_count) + attrs = { disabled_until: nil, backoff_count: 0, recent_failures: next_failure_count } + + assign_attributes(**attrs) + logger.info(hook_id: id, action: 'disable', **attrs) save(validate: false) end @@ -147,6 +152,10 @@ module WebHooks private + def logger + @logger ||= Gitlab::WebHooks::Logger.build + end + def next_failure_count recent_failures.succ.clamp(1, MAX_FAILURES) end diff --git a/app/models/integration.rb b/app/models/integration.rb index f823a385022..26fbd857764 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -167,7 +167,7 @@ class Integration < ApplicationRecord raise ArgumentError, "Unknown field storage: #{storage}" end - boolean_accessor(name) if attrs[:type] == 'checkbox' && storage != :attribute + boolean_accessor(name) if attrs[:type] == :checkbox && storage != :attribute end # :nocov: @@ -472,7 +472,7 @@ class Integration < ApplicationRecord # use `#secret?` here. # See: https://gitlab.com/groups/gitlab-org/-/epics/7652 def secret_fields - fields.select { |f| f[:type] == 'password' }.pluck(:name) + fields.select { |f| f[:type] == :password }.pluck(:name) end # Expose a list of fields in the JSON endpoint. @@ -517,7 +517,7 @@ class Integration < ApplicationRecord end def api_field_names - fields.reject { _1[:type] == 'password' || _1[:name] == 'webhook' }.pluck(:name) + fields.reject { _1[:type] == :password || _1[:name] == 'webhook' }.pluck(:name) end def form_fields diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb index a4036a82cec..6f96626718f 100644 --- a/app/models/integrations/apple_app_store.rb +++ b/app/models/integrations/apple_app_store.rb @@ -32,7 +32,7 @@ module Integrations field :app_store_private_key, api_only: true field :app_store_protected_refs, - type: 'checkbox', + type: :checkbox, section: SECTION_TYPE_CONFIGURATION, title: -> { s_('AppleAppStore|Protected branches and tags only') }, checkbox_label: -> { s_('AppleAppStore|Only set variables on protected branches and tags') } diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb index b8cfd718007..7436c08aa38 100644 --- a/app/models/integrations/asana.rb +++ b/app/models/integrations/asana.rb @@ -7,7 +7,7 @@ module Integrations validates :api_key, presence: true, if: :activated? field :api_key, - type: 'password', + type: :password, title: 'API key', help: -> { s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.') }, non_empty_password_title: -> { s_('ProjectService|Enter new API key') }, diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb index 536d5584bf6..6831fac32e6 100644 --- a/app/models/integrations/assembla.rb +++ b/app/models/integrations/assembla.rb @@ -5,7 +5,7 @@ module Integrations validates :token, presence: true, if: :activated? field :token, - type: 'password', + type: :password, non_empty_password_title: -> { s_('ProjectService|Enter new token') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, placeholder: '', diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb index 4638ca0c5f1..0b8432136dd 100644 --- a/app/models/integrations/bamboo.rb +++ b/app/models/integrations/bamboo.rb @@ -24,7 +24,7 @@ module Integrations help: -> { s_('BambooService|The user with API access to the Bamboo server.') } field :password, - type: 'password', + type: :password, non_empty_password_title: -> { s_('ProjectService|Enter new password') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password') } diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb index 7140e57961f..4d207574ca7 100644 --- a/app/models/integrations/base_chat_notification.rb +++ b/app/models/integrations/base_chat_notification.rb @@ -78,27 +78,27 @@ module Integrations def default_fields [ { - type: 'checkbox', + type: :checkbox, section: SECTION_TYPE_CONFIGURATION, name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze, { - type: 'select', + type: :select, section: SECTION_TYPE_CONFIGURATION, name: 'branches_to_be_notified', title: s_('Integrations|Branches for which notifications are to be sent'), choices: self.class.branch_choices }.freeze, { - type: 'text', + type: :text, section: SECTION_TYPE_CONFIGURATION, name: 'labels_to_be_notified', placeholder: '~backend,~frontend', help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.' }.freeze, { - type: 'select', + type: :select, section: SECTION_TYPE_CONFIGURATION, name: 'labels_to_be_notified_behavior', choices: [ @@ -110,8 +110,8 @@ module Integrations next unless requires_webhook? fields.unshift( - { type: 'text', name: 'webhook', help: webhook_help, required: true }.freeze, - { type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze + { type: :text, name: 'webhook', help: webhook_help, required: true }.freeze, + { type: :text, name: 'username', placeholder: 'GitLab-integration' }.freeze ) end.freeze end @@ -264,7 +264,7 @@ module Integrations def build_event_channels event_channel_names.map do |channel_field| - { type: 'text', name: channel_field, placeholder: default_channel_placeholder } + { type: :text, name: channel_field, placeholder: default_channel_placeholder } end end diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb index 5c08eac8557..6cd36e545a5 100644 --- a/app/models/integrations/buildkite.rb +++ b/app/models/integrations/buildkite.rb @@ -16,7 +16,7 @@ module Integrations required: true field :token, - type: 'password', + type: :password, title: -> { _('Token') }, help: -> do s_('ProjectService|The token you get after you create a Buildkite pipeline with a GitLab repository.') diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb index 9b837faf79b..007578e5830 100644 --- a/app/models/integrations/campfire.rb +++ b/app/models/integrations/campfire.rb @@ -13,7 +13,7 @@ module Integrations format: { with: SUBDOMAIN_REGEXP }, length: { in: 1..63 } field :token, - type: 'password', + type: :password, title: -> { _('Campfire token') }, help: -> { s_('CampfireService|API authentication token from Campfire.') }, non_empty_password_title: -> { s_('ProjectService|Enter new token') }, diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb index c7306209174..1a56763fe57 100644 --- a/app/models/integrations/datadog.rb +++ b/app/models/integrations/datadog.rb @@ -32,7 +32,7 @@ module Integrations help: -> { s_('DatadogIntegration|(Advanced) The full URL for your Datadog site.') } field :api_key, - type: 'password', + type: :password, title: -> { _('API key') }, non_empty_password_title: -> { s_('ProjectService|Enter new API key') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current API key') }, @@ -48,7 +48,7 @@ module Integrations field :archive_trace_events, storage: :attribute, - type: 'checkbox', + type: :checkbox, title: -> { _('Logs') }, checkbox_label: -> { _('Enable logs collection') }, help: -> { s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.') } @@ -73,7 +73,7 @@ module Integrations end field :datadog_tags, - type: 'textarea', + type: :textarea, title: -> { s_('DatadogIntegration|Tags') }, placeholder: "tag:value\nanother_tag:value", help: -> do diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb index 99072179c8c..7cae3ca20f9 100644 --- a/app/models/integrations/discord.rb +++ b/app/models/integrations/discord.rb @@ -14,11 +14,11 @@ module Integrations required: true field :notify_only_broken_pipelines, - type: 'checkbox', + type: :checkbox, section: SECTION_TYPE_CONFIGURATION field :branches_to_be_notified, - type: 'select', + type: :select, section: SECTION_TYPE_CONFIGURATION, title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb index 781acf65c47..ac464c020dd 100644 --- a/app/models/integrations/drone_ci.rb +++ b/app/models/integrations/drone_ci.rb @@ -16,7 +16,7 @@ module Integrations required: true field :token, - type: 'password', + type: :password, help: -> { s_('ProjectService|Token for the Drone project.') }, non_empty_password_title: -> { s_('ProjectService|Enter new token') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb index 25bda8c2bf0..eb893ae45d0 100644 --- a/app/models/integrations/emails_on_push.rb +++ b/app/models/integrations/emails_on_push.rb @@ -10,7 +10,7 @@ module Integrations validate :number_of_recipients_within_limit, if: :validate_recipients? field :send_from_committer_email, - type: 'checkbox', + type: :checkbox, title: -> { s_("EmailsOnPushService|Send from committer") }, help: -> do @help ||= begin @@ -21,17 +21,17 @@ module Integrations end field :disable_diffs, - type: 'checkbox', + type: :checkbox, title: -> { s_("EmailsOnPushService|Disable code diffs") }, help: -> { s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") } field :branches_to_be_notified, - type: 'select', + type: :select, title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: branch_choices field :recipients, - type: 'textarea', + type: :textarea, placeholder: -> { s_('EmailsOnPushService|tanuki@example.com gitlab@example.com') }, help: -> { s_('EmailsOnPushService|Emails separated by whitespace.') } diff --git a/app/models/integrations/field.rb b/app/models/integrations/field.rb index 9f2274216f6..11fac45323e 100644 --- a/app/models/integrations/field.rb +++ b/app/models/integrations/field.rb @@ -11,15 +11,15 @@ module Integrations non_empty_password_title ].concat(BOOLEAN_ATTRIBUTES).freeze - TYPES = %w[text textarea password checkbox select].freeze + TYPES = %i[text textarea password checkbox select].freeze attr_reader :name, :integration_class - def initialize(name:, integration_class:, type: 'text', is_secret: false, api_only: false, **attributes) + def initialize(name:, integration_class:, type: :text, is_secret: false, api_only: false, **attributes) @name = name.to_s.freeze @integration_class = integration_class - attributes[:type] = is_secret ? 'password' : type + attributes[:type] = is_secret ? :password : type attributes[:api_only] = api_only attributes[:is_secret] = is_secret @attributes = attributes.freeze @@ -42,7 +42,7 @@ module Integrations end def secret? - self[:type] == 'password' + self[:type] == :password end ATTRIBUTES.each do |name| diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb index 7ba9bbc38e6..037c689c75e 100644 --- a/app/models/integrations/hangouts_chat.rb +++ b/app/models/integrations/hangouts_chat.rb @@ -10,11 +10,11 @@ module Integrations required: true field :notify_only_broken_pipelines, - type: 'checkbox', + type: :checkbox, section: SECTION_TYPE_CONFIGURATION field :branches_to_be_notified, - type: 'select', + type: :select, section: SECTION_TYPE_CONFIGURATION, title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb index 079811e0df0..559e48afd10 100644 --- a/app/models/integrations/harbor.rb +++ b/app/models/integrations/harbor.rb @@ -25,7 +25,7 @@ module Integrations required: true field :password, - type: 'password', + type: :password, title: -> { s_('HarborIntegration|Harbor password') }, help: -> { s_('HarborIntegration|Password for your Harbor username.') }, non_empty_password_title: -> { s_('HarborIntegration|Enter new Harbor password') }, diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb index 3f3e321f45e..a54946f074a 100644 --- a/app/models/integrations/irker.rb +++ b/app/models/integrations/irker.rb @@ -23,7 +23,7 @@ module Integrations placeholder: 'irc://irc.network.net:6697/' field :recipients, - type: 'textarea', + type: :textarea, title: -> { s_('IrkerService|Recipients') }, placeholder: 'irc[s]://irc.network.net[:port]/#channel', required: true, @@ -45,7 +45,7 @@ module Integrations end field :colorize_messages, - type: 'checkbox', + type: :checkbox, title: -> { _('Colorize messages') } # NOTE: This field is only used internally to store the parsed diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb index d2e8393ef95..7769ea7d2dd 100644 --- a/app/models/integrations/jenkins.rb +++ b/app/models/integrations/jenkins.rb @@ -22,7 +22,7 @@ module Integrations help: -> { s_('The username for the Jenkins server.') } field :password, - type: 'password', + type: :password, help: -> { s_('The password for the Jenkins server.') }, non_empty_password_title: -> { s_('ProjectService|Enter new password.') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password.') } diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index 4e0c2dde13b..faf0a378a17 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -74,7 +74,7 @@ module Integrations exposes_secrets: true field :jira_auth_type, - type: 'select', + type: :select, required: true, section: SECTION_TYPE_CONNECTION, title: -> { s_('JiraService|Authentication type') }, diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb index e075400d9b5..73cddd163e0 100644 --- a/app/models/integrations/mattermost_slash_commands.rb +++ b/app/models/integrations/mattermost_slash_commands.rb @@ -5,7 +5,7 @@ module Integrations include Ci::TriggersHelper field :token, - type: 'password', + type: :password, non_empty_password_title: -> { s_('ProjectService|Enter new token') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, placeholder: '' diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb index a9ed0bd3da1..25308948d51 100644 --- a/app/models/integrations/microsoft_teams.rb +++ b/app/models/integrations/microsoft_teams.rb @@ -10,12 +10,12 @@ module Integrations required: true field :notify_only_broken_pipelines, - type: 'checkbox', + type: :checkbox, section: SECTION_TYPE_CONFIGURATION, help: 'If selected, successful pipelines do not trigger a notification event.' field :branches_to_be_notified, - type: 'select', + type: :select, section: SECTION_TYPE_CONFIGURATION, title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb index 3973b492b6d..c9c08ec9771 100644 --- a/app/models/integrations/packagist.rb +++ b/app/models/integrations/packagist.rb @@ -11,7 +11,7 @@ module Integrations required: true field :token, - type: 'password', + type: :password, title: -> { _('Token') }, help: -> { _('Enter your Packagist token.') }, non_empty_password_title: -> { s_('ProjectService|Enter new token') }, diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb index 55a8ce0be11..fa22bd1a73c 100644 --- a/app/models/integrations/pipelines_email.rb +++ b/app/models/integrations/pipelines_email.rb @@ -10,19 +10,19 @@ module Integrations validate :number_of_recipients_within_limit, if: :validate_recipients? field :recipients, - type: 'textarea', + type: :textarea, help: -> { _('Comma-separated list of email addresses.') }, required: true field :notify_only_broken_pipelines, - type: 'checkbox' + type: :checkbox field :notify_only_default_branch, - type: 'checkbox', + type: :checkbox, api_only: true field :branches_to_be_notified, - type: 'select', + type: :select, title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: branch_choices diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb index 1acdbbbf9bc..0d9a3f05a86 100644 --- a/app/models/integrations/pivotaltracker.rb +++ b/app/models/integrations/pivotaltracker.rb @@ -7,7 +7,7 @@ module Integrations validates :token, presence: true, if: :activated? field :token, - type: 'password', + type: :password, help: -> { s_('PivotalTrackerService|Pivotal Tracker API token. User must have access to the story. All comments are attributed to this user.') }, non_empty_password_title: -> { s_('ProjectService|Enter new token') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb index 8969c6c13b2..736318ed707 100644 --- a/app/models/integrations/prometheus.rb +++ b/app/models/integrations/prometheus.rb @@ -6,7 +6,7 @@ module Integrations include Gitlab::Utils::StrongMemoize field :manual_configuration, - type: 'checkbox', + type: :checkbox, title: -> { s_('PrometheusService|Active') }, help: -> { s_('PrometheusService|Select this checkbox to override the auto configuration settings with your own settings.') }, required: true @@ -24,7 +24,7 @@ module Integrations required: false field :google_iap_service_account_json, - type: 'textarea', + type: :textarea, title: 'Google IAP Service Account JSON', placeholder: -> { s_('PrometheusService|{ "type": "service_account", "project_id": ... }') }, help: -> { s_('PrometheusService|The contents of the credentials.json file of your service account.') }, diff --git a/app/models/integrations/pumble.rb b/app/models/integrations/pumble.rb index 85fb3b9c3e6..8f0dddcc5c5 100644 --- a/app/models/integrations/pumble.rb +++ b/app/models/integrations/pumble.rb @@ -10,12 +10,12 @@ module Integrations required: true field :notify_only_broken_pipelines, - type: 'checkbox', + type: :checkbox, section: SECTION_TYPE_CONFIGURATION, help: 'If selected, successful pipelines do not trigger a notification event.' field :branches_to_be_notified, - type: 'select', + type: :select, section: SECTION_TYPE_CONFIGURATION, title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb index 6bb6b6d60f6..006b731c6c2 100644 --- a/app/models/integrations/pushover.rb +++ b/app/models/integrations/pushover.rb @@ -7,7 +7,7 @@ module Integrations validates :api_key, :user_key, :priority, presence: true, if: :activated? field :api_key, - type: 'password', + type: :password, title: -> { _('API key') }, help: -> { s_('PushoverService|Enter your application key.') }, non_empty_password_title: -> { s_('ProjectService|Enter new API key') }, @@ -16,7 +16,7 @@ module Integrations required: true field :user_key, - type: 'password', + type: :password, title: -> { _('User key') }, help: -> { s_('PushoverService|Enter your user key.') }, non_empty_password_title: -> { s_('PushoverService|Enter new user key') }, @@ -30,7 +30,7 @@ module Integrations placeholder: '' field :priority, - type: 'select', + type: :select, required: true, choices: -> do [ @@ -42,7 +42,7 @@ module Integrations end field :sound, - type: 'select', + type: :select, choices: -> do [ ['Device default sound', nil], diff --git a/app/models/integrations/slack_slash_commands.rb b/app/models/integrations/slack_slash_commands.rb index 343c8d68166..b209f37ee7c 100644 --- a/app/models/integrations/slack_slash_commands.rb +++ b/app/models/integrations/slack_slash_commands.rb @@ -5,7 +5,7 @@ module Integrations include Ci::TriggersHelper field :token, - type: 'password', + type: :password, non_empty_password_title: -> { s_('ProjectService|Enter new token') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, placeholder: '' diff --git a/app/models/integrations/squash_tm.rb b/app/models/integrations/squash_tm.rb index e0a63b5ae6a..bf3f391564f 100644 --- a/app/models/integrations/squash_tm.rb +++ b/app/models/integrations/squash_tm.rb @@ -11,7 +11,7 @@ module Integrations required: true field :token, - type: 'password', + type: :password, title: -> { s_('SquashTmIntegration|Secret token (optional)') }, non_empty_password_title: -> { s_('ProjectService|Enter new token') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb index af629d6ef1e..c74e0aab030 100644 --- a/app/models/integrations/teamcity.rb +++ b/app/models/integrations/teamcity.rb @@ -22,7 +22,7 @@ module Integrations help: -> { s_('ProjectService|Must have permission to trigger a manual build in TeamCity.') } field :password, - type: 'password', + type: :password, non_empty_password_title: -> { s_('ProjectService|Enter new password') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current password') } diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb index 6c447c8f4e4..6de693b5278 100644 --- a/app/models/integrations/unify_circuit.rb +++ b/app/models/integrations/unify_circuit.rb @@ -10,11 +10,11 @@ module Integrations required: true field :notify_only_broken_pipelines, - type: 'checkbox', + type: :checkbox, section: SECTION_TYPE_CONFIGURATION field :branches_to_be_notified, - type: 'select', + type: :select, section: SECTION_TYPE_CONFIGURATION, title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb index ef1bc81ea58..21c65cc2b32 100644 --- a/app/models/integrations/webex_teams.rb +++ b/app/models/integrations/webex_teams.rb @@ -10,11 +10,11 @@ module Integrations required: true field :notify_only_broken_pipelines, - type: 'checkbox', + type: :checkbox, section: SECTION_TYPE_CONFIGURATION field :branches_to_be_notified, - type: 'select', + type: :select, section: SECTION_TYPE_CONFIGURATION, title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb index 459756c865b..fd2c741bd6b 100644 --- a/app/models/integrations/zentao.rb +++ b/app/models/integrations/zentao.rb @@ -19,7 +19,7 @@ module Integrations exposes_secrets: true field :api_token, - type: 'password', + type: :password, title: -> { s_('ZentaoIntegration|ZenTao API token') }, non_empty_password_title: -> { s_('ZentaoIntegration|Enter new ZenTao API token') }, non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, diff --git a/app/models/note.rb b/app/models/note.rb index 913d008003c..f1760a8dc4a 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -197,9 +197,7 @@ class Note < ApplicationRecord # Syncs `confidential` with `internal` as we rename the column. # https://gitlab.com/gitlab-org/gitlab/-/issues/367923 before_create :set_internal_flag - after_destroy :expire_etag_cache after_save :keep_around_commit, if: :for_project_noteable?, unless: -> { importing? || skip_keep_around_commits } - after_save :expire_etag_cache, unless: :importing? after_save :touch_noteable, unless: :importing? after_commit :notify_after_create, on: :create after_commit :notify_after_destroy, on: :destroy @@ -207,6 +205,7 @@ class Note < ApplicationRecord after_commit :trigger_note_subscription_create, on: :create after_commit :trigger_note_subscription_update, on: :update after_commit :trigger_note_subscription_destroy, on: :destroy + after_commit :expire_etag_cache, unless: :importing? def trigger_note_subscription_create return unless trigger_note_subscription? diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index cb578496f26..99128d3cddf 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class ProjectAuthorization < ApplicationRecord - BATCH_SIZE = 1000 - SLEEP_DELAY = 0.1 - extend SuppressCompositePrimaryKeyWarning include FromUnion @@ -28,57 +25,6 @@ class ProjectAuthorization < ApplicationRecord def self.insert_all(attributes) super(attributes, unique_by: connection.schema_cache.primary_keys(table_name)) end - - def self.insert_all_in_batches(attributes, per_batch = BATCH_SIZE) - add_delay = add_delay_between_batches?(entire_size: attributes.size, batch_size: per_batch) - log_details(entire_size: attributes.size, batch_size: per_batch) if add_delay - - attributes.each_slice(per_batch) do |attributes_batch| - insert_all(attributes_batch) - perform_delay if add_delay - end - end - - def self.delete_all_in_batches_for_project(project:, user_ids:, per_batch: BATCH_SIZE) - add_delay = add_delay_between_batches?(entire_size: user_ids.size, batch_size: per_batch) - log_details(entire_size: user_ids.size, batch_size: per_batch) if add_delay - - user_ids.each_slice(per_batch) do |user_ids_batch| - project.project_authorizations.where(user_id: user_ids_batch).delete_all - perform_delay if add_delay - end - end - - def self.delete_all_in_batches_for_user(user:, project_ids:, per_batch: BATCH_SIZE) - add_delay = add_delay_between_batches?(entire_size: project_ids.size, batch_size: per_batch) - log_details(entire_size: project_ids.size, batch_size: per_batch) if add_delay - - project_ids.each_slice(per_batch) do |project_ids_batch| - user.project_authorizations.where(project_id: project_ids_batch).delete_all - perform_delay if add_delay - end - end - - private_class_method def self.add_delay_between_batches?(entire_size:, batch_size:) - # The reason for adding a delay is to give the replica database enough time to - # catch up with the primary when large batches of records are being added/removed. - # Hance, we add a delay only if the GitLab installation has a replica database configured. - entire_size > batch_size && - !::Gitlab::Database::LoadBalancing.primary_only? - end - - private_class_method def self.log_details(entire_size:, batch_size:) - Gitlab::AppLogger.info( - entire_size: entire_size, - total_delay: (entire_size / batch_size.to_f).ceil * SLEEP_DELAY, - message: 'Project authorizations refresh performed with delay', - **Gitlab::ApplicationContext.current - ) - end - - private_class_method def self.perform_delay - sleep(SLEEP_DELAY) - end end ProjectAuthorization.prepend_mod_with('ProjectAuthorization') diff --git a/app/models/project_authorizations/changes.rb b/app/models/project_authorizations/changes.rb new file mode 100644 index 00000000000..05a61dabd96 --- /dev/null +++ b/app/models/project_authorizations/changes.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +module ProjectAuthorizations + # How to use this class + # authorizations_to_add: + # Rows to insert in the form `[{ user_id: user_id, project_id: project_id, access_level: access_level}, ...] + # + # ProjectAuthorizations::Changes.new do |changes| + # changes.add(authorizations_to_add) + # changes.remove_users_in_project(project, user_ids) + # changes.remove_projects_for_user(user, project_ids) + # end.apply! + class Changes + attr_reader :projects_to_remove, :users_to_remove, :authorizations_to_add + + BATCH_SIZE = 1000 + SLEEP_DELAY = 0.1 + + def initialize + @authorizations_to_add = [] + yield self + end + + def add(authorizations_to_add) + @authorizations_to_add += authorizations_to_add + end + + def remove_users_in_project(project, user_ids) + @users_to_remove = { user_ids: user_ids, scope: project } + end + + def remove_projects_for_user(user, project_ids) + @projects_to_remove = { project_ids: project_ids, scope: user } + end + + def apply! + delete_authorizations_for_user if should_delete_authorizations_for_user? + delete_authorizations_for_project if should_delete_authorizations_for_project? + add_authorizations if should_add_authorization? + end + + private + + def should_add_authorization? + authorizations_to_add.present? + end + + def should_delete_authorizations_for_user? + user && project_ids.present? + end + + def should_delete_authorizations_for_project? + project && user_ids.present? + end + + def add_authorizations + insert_all_in_batches(authorizations_to_add) + end + + def delete_authorizations_for_user + delete_all_in_batches(resource: user, + ids_to_remove: project_ids, + column_name_of_ids_to_remove: :project_id) + end + + def delete_authorizations_for_project + delete_all_in_batches(resource: project, + ids_to_remove: user_ids, + column_name_of_ids_to_remove: :user_id) + end + + def delete_all_in_batches(resource:, ids_to_remove:, column_name_of_ids_to_remove:) + add_delay = add_delay_between_batches?(entire_size: ids_to_remove.size, batch_size: BATCH_SIZE) + log_details(entire_size: ids_to_remove.size, batch_size: BATCH_SIZE) if add_delay + + ids_to_remove.each_slice(BATCH_SIZE) do |ids_batch| + resource.project_authorizations.where(column_name_of_ids_to_remove => ids_batch).delete_all + perform_delay if add_delay + end + end + + def insert_all_in_batches(attributes) + add_delay = add_delay_between_batches?(entire_size: attributes.size, batch_size: BATCH_SIZE) + log_details(entire_size: attributes.size, batch_size: BATCH_SIZE) if add_delay + + attributes.each_slice(BATCH_SIZE) do |attributes_batch| + ProjectAuthorization.insert_all(attributes_batch) + perform_delay if add_delay + end + end + + def add_delay_between_batches?(entire_size:, batch_size:) + # The reason for adding a delay is to give the replica database enough time to + # catch up with the primary when large batches of records are being added/removed. + # Hence, we add a delay only if the GitLab installation has a replica database configured. + entire_size > batch_size && + !::Gitlab::Database::LoadBalancing.primary_only? + end + + def log_details(entire_size:, batch_size:) + Gitlab::AppLogger.info( + entire_size: entire_size, + total_delay: (entire_size / batch_size.to_f).ceil * SLEEP_DELAY, + message: 'Project authorizations refresh performed with delay', + **Gitlab::ApplicationContext.current + ) + end + + def perform_delay + sleep(SLEEP_DELAY) + end + + def user + projects_to_remove&.[](:scope) + end + + def project_ids + projects_to_remove&.[](:project_ids) + end + + def project + users_to_remove&.[](:scope) + end + + def user_ids + users_to_remove&.[](:user_ids) + end + end +end diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 365bb5237c3..18bbfc894e7 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -67,7 +67,13 @@ class ProjectStatistics < ApplicationRecord end def update_repository_size - self.repository_size = project.repository.size * 1.megabyte + size = if Feature.enabled?(:recent_objects_for_project_statistics, project) + project.repository.recent_objects_size + else + project.repository.size + end + + self.repository_size = size.megabytes end def update_wiki_size diff --git a/app/models/repository.rb b/app/models/repository.rb index 1321c9da780..cd269f928b5 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -47,7 +47,7 @@ class Repository # # For example, for entry `:commit_count` there's a method called `commit_count` which # stores its data in the `commit_count` cache key. - CACHED_METHODS = %i(size commit_count readme_path contribution_guide + CACHED_METHODS = %i(size recent_objects_size commit_count readme_path contribution_guide changelog license_blob license_gitaly gitignore gitlab_ci_yml branch_names tag_names branch_count tag_count avatar exists? root_ref merged_branch_names @@ -363,7 +363,7 @@ class Repository end def expire_statistics_caches - expire_method_caches(%i(size commit_count)) + expire_method_caches(%i(size recent_objects_size commit_count)) end def expire_all_method_caches @@ -579,6 +579,12 @@ class Repository end cache_method :size, fallback: 0.0 + # The recent objects size of this repository in mebibytes. + def recent_objects_size + exists? ? raw_repository.recent_objects_size : 0.0 + end + cache_method :recent_objects_size, fallback: 0.0 + def commit_count root_ref ? raw_repository.commit_count(root_ref) : 0 end diff --git a/app/policies/ci/bridge_policy.rb b/app/policies/ci/bridge_policy.rb index 37a07ea8aaf..5f9e8eab08a 100644 --- a/app/policies/ci/bridge_policy.rb +++ b/app/policies/ci/bridge_policy.rb @@ -2,6 +2,8 @@ module Ci class BridgePolicy < CommitStatusPolicy + include Ci::DeployablePolicy + condition(:can_update_downstream_branch) do ::Gitlab::UserAccess.new(@user, container: @subject.downstream_project) .can_update_branch?(@subject.target_revision_ref) diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 0f551234efe..bce7ceafe17 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -2,6 +2,8 @@ module Ci class BuildPolicy < CommitStatusPolicy + include Ci::DeployablePolicy + delegate { @subject.project } condition(:protected_ref) do @@ -22,15 +24,6 @@ module Ci end end - # overridden in EE - condition(:protected_environment) do - false - end - - condition(:outdated_deployment) do - @subject.outdated_deployment? - end - condition(:owner_of_job) do @subject.triggered_by?(@user) end @@ -83,17 +76,15 @@ module Ci # Authorizing the user to access to protected entities. # There is a "jailbreak" mode to exceptionally bypass the authorization, # however, you should NEVER allow it, rather suspect it's a wrong feature/product design. - rule { ~can?(:jailbreak) & (archived | (protected_ref & ~admin) | protected_environment) }.policy do + rule { ~can?(:jailbreak) & (archived | (protected_ref & ~admin)) }.policy do prevent :update_commit_status end - rule { ~can?(:jailbreak) & (archived | protected_ref | protected_environment) }.policy do + rule { ~can?(:jailbreak) & (archived | protected_ref) }.policy do prevent :update_build prevent :erase_build end - rule { outdated_deployment }.prevent :update_build - rule { can?(:admin_build) | (can?(:update_build) & owner_of_job & unprotected_ref) }.enable :erase_build rule { can?(:public_access) & branch_allows_collaboration }.policy do diff --git a/app/policies/ci/deployable_policy.rb b/app/policies/ci/deployable_policy.rb new file mode 100644 index 00000000000..f0105b001f2 --- /dev/null +++ b/app/policies/ci/deployable_policy.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Ci + module DeployablePolicy + extend ActiveSupport::Concern + + included do + prepend_mod_with('Ci::DeployablePolicy') # rubocop: disable Cop/InjectEnterpriseEditionModule + + condition(:outdated_deployment) do + @subject.outdated_deployment? + end + + rule { outdated_deployment }.prevent :update_build + end + end +end diff --git a/app/serializers/integrations/field_entity.rb b/app/serializers/integrations/field_entity.rb index 1c548cfab78..dc2ec55d073 100644 --- a/app/serializers/integrations/field_entity.rb +++ b/app/serializers/integrations/field_entity.rb @@ -5,12 +5,16 @@ module Integrations include RequestAwareEntity include Gitlab::Utils::StrongMemoize - expose :section, :type, :name, :placeholder, :required, :choices, :checkbox_label + expose :section, :name, :placeholder, :required, :choices, :checkbox_label expose :title do |field| non_empty_password?(field) ? field[:non_empty_password_title] : field[:title] end + expose :type do |field| + field[:type].to_s + end + expose :help do |field| non_empty_password?(field) ? field[:non_empty_password_help] : field[:help] end @@ -20,7 +24,7 @@ module Integrations if non_empty_password?(field) 'true' - elsif field[:type] == 'checkbox' + elsif field[:type] == :checkbox ActiveRecord::Type::Boolean.new.deserialize(value).to_s elsif field[:name] == 'webhook' && integration.chat? BaseChatNotification::SECRET_MASK if value.present? @@ -44,7 +48,7 @@ module Integrations def non_empty_password?(field) strong_memoize(:non_empty_password) do - field[:type] == 'password' && value_for(field).present? + field[:type] == :password && value_for(field).present? end end end diff --git a/app/services/admin/plan_limits/update_service.rb b/app/services/admin/plan_limits/update_service.rb index cda9a7e7f8c..24ce3c4095f 100644 --- a/app/services/admin/plan_limits/update_service.rb +++ b/app/services/admin/plan_limits/update_service.rb @@ -15,6 +15,12 @@ module Admin add_history_to_params! + plan_limits.assign_attributes(parsed_params) + + validate_storage_limits + + return error(plan_limits.errors.full_messages, :bad_request) if plan_limits.errors.any? + if plan_limits.update(parsed_params) success else @@ -26,6 +32,8 @@ module Admin attr_accessor :current_user, :params, :plan, :plan_limits + delegate :notification_limit, :storage_size_limit, :enforcement_limit, to: :plan_limits + def can_update? current_user.can_admin_all_resources? end @@ -35,6 +43,39 @@ module Admin parsed_params.merge!(limits_history: formatted_limits_history) unless formatted_limits_history.empty? end + def validate_storage_limits + validate_notification_limit + validate_enforcement_limit + validate_storage_size_limit + end + + def validate_notification_limit + return unless parsed_params.include?(:notification_limit) + return if notification_limit >= storage_size_limit && notification_limit <= enforcement_limit + + plan_limits.errors.add(:notification_limit, "must be greater than or equal to " \ + "storage_size_limit (Dashboard limit): #{storage_size_limit} " \ + "and less than or equal to enforcement_limit: #{enforcement_limit}") + end + + def validate_enforcement_limit + return unless parsed_params.include?(:enforcement_limit) + return if enforcement_limit >= storage_size_limit && enforcement_limit >= notification_limit + + plan_limits.errors.add(:enforcement_limit, "must be greater than or equal to " \ + "storage_size_limit (Dashboard limit): #{storage_size_limit} and " \ + "greater than or equal to notification_limit: #{notification_limit}") + end + + def validate_storage_size_limit + return unless parsed_params.include?(:storage_size_limit) + return if storage_size_limit <= enforcement_limit && storage_size_limit <= notification_limit + + plan_limits.errors.add(:storage_size_limit, "(Dashboard limit) must be less than or equal to " \ + "enforcement_limit: #{enforcement_limit} " \ + "and notification_limit: #{notification_limit}") + end + # Overridden in EE def parsed_params params diff --git a/app/services/authorized_project_update/project_recalculate_service.rb b/app/services/authorized_project_update/project_recalculate_service.rb index cb83dc57478..535845b9f94 100644 --- a/app/services/authorized_project_update/project_recalculate_service.rb +++ b/app/services/authorized_project_update/project_recalculate_service.rb @@ -64,13 +64,10 @@ module AuthorizedProjectUpdate end def refresh_authorizations - if user_ids_to_remove.any? - ProjectAuthorization.delete_all_in_batches_for_project( - project: project, - user_ids: user_ids_to_remove) - end - - ProjectAuthorization.insert_all_in_batches(authorizations_to_create) if authorizations_to_create.any? + ProjectAuthorizations::Changes.new do |changes| + changes.add(authorizations_to_create) + changes.remove_users_in_project(project, user_ids_to_remove) + end.apply! end def apply_scopes(project_authorizations) diff --git a/app/services/metrics/dashboard/base_service.rb b/app/services/metrics/dashboard/base_service.rb index 7863af5affc..06cd0c46560 100644 --- a/app/services/metrics/dashboard/base_service.rb +++ b/app/services/metrics/dashboard/base_service.rb @@ -10,7 +10,6 @@ module Metrics STAGES = ::Gitlab::Metrics::Dashboard::Stages SEQUENCE = [ STAGES::CommonMetricsInserter, - STAGES::VariableEndpointInserter, STAGES::PanelIdsInserter, STAGES::TrackPanelType, STAGES::UrlValidator diff --git a/app/services/metrics/dashboard/default_embed_service.rb b/app/services/metrics/dashboard/default_embed_service.rb deleted file mode 100644 index 30a8150d6be..00000000000 --- a/app/services/metrics/dashboard/default_embed_service.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -# Responsible for returning a filtered system dashboard -# containing only the default embedded metrics. This class -# operates by selecting metrics directly from the system -# dashboard. -# -# Why isn't this filtering in a processing stage? By filtering -# here, we ensure the dynamically-determined dashboard is cached. -# -# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards. -module Metrics - module Dashboard - class DefaultEmbedService < ::Metrics::Dashboard::BaseEmbedService - # For the default filtering for embedded metrics, - # uses the 'id' key in dashboard-yml definition for - # identification. - DEFAULT_EMBEDDED_METRICS_IDENTIFIERS = %w( - system_metrics_kubernetes_container_memory_total - system_metrics_kubernetes_container_cores_total - ).freeze - - class << self - def valid_params?(params) - embedded?(params[:embedded]) - end - end - - # Returns a new dashboard with only the matching - # metrics from the system dashboard, stripped of groups. - # @return [Hash] - def get_raw_dashboard - panels = panel_groups.each_with_object([]) do |group, panels| - matched_panels = group['panels'].select { |panel| matching_panel?(panel) } - - panels.concat(matched_panels) - end - - { 'panel_groups' => [{ 'panels' => panels }] } - end - - private - - # Returns an array of the panels groups on the - # system dashboard - def panel_groups - ::Metrics::Dashboard::SystemDashboardService - .new(project, nil) - .raw_dashboard['panel_groups'] - end - - # Identifies a panel as "matching" if any metric ids in - # the panel is in the list of identifiers to collect. - def matching_panel?(panel) - panel['metrics'].any? do |metric| - metric_identifiers.include?(metric['id']) - end - end - - def metric_identifiers - DEFAULT_EMBEDDED_METRICS_IDENTIFIERS - end - - def identifiers - metric_identifiers.join('|') - end - end - end -end diff --git a/app/services/metrics/dashboard/predefined_dashboard_service.rb b/app/services/metrics/dashboard/predefined_dashboard_service.rb index 1777a94bdd0..32d34474a08 100644 --- a/app/services/metrics/dashboard/predefined_dashboard_service.rb +++ b/app/services/metrics/dashboard/predefined_dashboard_service.rb @@ -10,7 +10,6 @@ module Metrics DASHBOARD_NAME = nil SEQUENCE = [ - STAGES::VariableEndpointInserter, STAGES::PanelIdsInserter ].freeze diff --git a/app/services/metrics/dashboard/system_dashboard_service.rb b/app/services/metrics/dashboard/system_dashboard_service.rb index b84cf187d2c..2bc19fcb9e2 100644 --- a/app/services/metrics/dashboard/system_dashboard_service.rb +++ b/app/services/metrics/dashboard/system_dashboard_service.rb @@ -15,7 +15,6 @@ module Metrics STAGES::CommonMetricsInserter, STAGES::CustomMetricsInserter, STAGES::CustomMetricsDetailsInserter, - STAGES::VariableEndpointInserter, STAGES::PanelIdsInserter ].freeze diff --git a/app/services/projects/update_statistics_service.rb b/app/services/projects/update_statistics_service.rb index 71f5a8e633d..9b979f6ed68 100644 --- a/app/services/projects/update_statistics_service.rb +++ b/app/services/projects/update_statistics_service.rb @@ -5,7 +5,7 @@ module Projects include ::Gitlab::Utils::StrongMemoize STAT_TO_CACHED_METHOD = { - repository_size: :size, + repository_size: [:size, :recent_objects_size], commit_count: :commit_count }.freeze @@ -37,7 +37,7 @@ module Projects def method_caches_to_expire strong_memoize(:method_caches_to_expire) do - statistics.map { |stat| STAT_TO_CACHED_METHOD[stat] }.compact + statistics.flat_map { |stat| STAT_TO_CACHED_METHOD[stat] }.compact end end diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index b1ffd006795..197260a80ca 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -67,8 +67,10 @@ module Users def update_authorizations(remove = [], add = []) log_refresh_details(remove, add) - ProjectAuthorization.delete_all_in_batches_for_user(user: user, project_ids: remove) if remove.any? - ProjectAuthorization.insert_all_in_batches(add) if add.any? + ProjectAuthorizations::Changes.new do |changes| + changes.add(add) + changes.remove_projects_for_user(user, remove) + end.apply! # Since we batch insert authorization rows, Rails' associations may get # out of sync. As such we force a reload of the User object. diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index fc05f88c763..d8d6af606ac 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -33,6 +33,9 @@ .form-group = f.label :bulk_import_max_download_file_size, s_('BulkImport|Direct transfer maximum download file size (MB)'), class: 'label-light' = f.number_field :bulk_import_max_download_file_size, class: 'form-control gl-form-input', title: s_('BulkImport|Maximum download file size when importing from source GitLab instances by direct transfer.'), data: { toggle: 'tooltip', container: 'body' } + .form-group + = f.label :max_decompressed_archive_size, s_('Import|Maximum decompressed size (MiB)'), class: 'label-light' + = f.number_field :max_decompressed_archive_size, class: 'form-control gl-form-input', title: s_('Import|Maximum size of decompressed archive.'), data: { toggle: 'tooltip', container: 'body' } %span.form-text.text-muted= _('Set to 0 for no size limit.') .form-group = f.label :session_expire_delay, _('Session duration (minutes)'), class: 'label-light' diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml index c22eeba2f01..1760e6e0f84 100644 --- a/app/views/devise/confirmations/almost_there.haml +++ b/app/views/devise/confirmations/almost_there.haml @@ -8,6 +8,8 @@ = render "layouts/bizible" = render "layouts/google_tag_manager_body" += render_if_exists 'devise/shared/delete_unconfirmed_users_flash' + .well-confirmation.gl-text-center.gl-mb-6 %h1.gl-mt-0 = _("Almost there...") diff --git a/app/views/devise/mailer/_confirmation_instructions_account.html.haml b/app/views/devise/mailer/_confirmation_instructions_account.html.haml index c1655818770..ff5027b8464 100644 --- a/app/views/devise/mailer/_confirmation_instructions_account.html.haml +++ b/app/views/devise/mailer/_confirmation_instructions_account.html.haml @@ -1,9 +1,11 @@ - confirmation_link = confirmation_url(@resource, confirmation_token: @token) + - if @resource.unconfirmed_email.present? || !@resource.created_recently? #content = email_default_heading(@email) %p= _('Click the link below to confirm your email address.') #cta + = render_if_exists 'devise/shared/delete_unconfirmed_users' = link_to _('Confirm your email address'), confirmation_link - else #content @@ -13,4 +15,5 @@ = email_default_heading(_("Welcome, %{name}!") % { name: @resource.name }) %p= _("To get started, click the link below to confirm your account.") #cta + = render_if_exists 'devise/shared/delete_unconfirmed_users' = link_to _('Confirm your account'), confirmation_link diff --git a/app/views/devise/mailer/_confirmation_instructions_account.text.erb b/app/views/devise/mailer/_confirmation_instructions_account.text.erb index 7e4f38885f6..7436da66e63 100644 --- a/app/views/devise/mailer/_confirmation_instructions_account.text.erb +++ b/app/views/devise/mailer/_confirmation_instructions_account.text.erb @@ -10,4 +10,6 @@ <%= _('To get started, use the link below to confirm your account.') %> <% end %> +<%= render_if_exists 'devise/shared/delete_unconfirmed_users_text' %> + <%= confirmation_url(@resource, confirmation_token: @token) %> diff --git a/app/workers/web_hook_worker.rb b/app/workers/web_hook_worker.rb index 043a16e3527..cea0816f5a6 100644 --- a/app/workers/web_hook_worker.rb +++ b/app/workers/web_hook_worker.rb @@ -24,7 +24,10 @@ class WebHookWorker # present in the request header so the hook can pass this same header value in its request. Gitlab::WebHooks::RecursionDetection.set_request_uuid(params[:recursion_detection_request_uuid]) - WebHookService.new(hook, data, hook_name, jid).execute + WebHookService.new(hook, data, hook_name, jid).execute.tap do |response| + log_extra_metadata_on_done(:response_status, response.status) + log_extra_metadata_on_done(:http_status, response[:http_status]) + end end end # rubocop:enable Scalability/IdempotentWorker diff --git a/config/feature_flags/development/action_cable_notes.yml b/config/feature_flags/development/action_cable_notes.yml new file mode 100644 index 00000000000..d41b1e444eb --- /dev/null +++ b/config/feature_flags/development/action_cable_notes.yml @@ -0,0 +1,8 @@ +--- +name: action_cable_notes +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127964 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412823 +milestone: '16.3' +type: development +group: group::project management +default_enabled: false diff --git a/config/feature_flags/development/recent_objects_for_project_statistics.yml b/config/feature_flags/development/recent_objects_for_project_statistics.yml new file mode 100644 index 00000000000..1c66cc492b5 --- /dev/null +++ b/config/feature_flags/development/recent_objects_for_project_statistics.yml @@ -0,0 +1,8 @@ +--- +name: recent_objects_for_project_statistics +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127867 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/420125 +milestone: '16.3' +type: development +group: group::utilization +default_enabled: false diff --git a/db/migrate/20230802065830_add_max_decompression_archive_size_to_application_settings.rb b/db/migrate/20230802065830_add_max_decompression_archive_size_to_application_settings.rb new file mode 100644 index 00000000000..c2ed6204b0a --- /dev/null +++ b/db/migrate/20230802065830_add_max_decompression_archive_size_to_application_settings.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddMaxDecompressionArchiveSizeToApplicationSettings < Gitlab::Database::Migration[2.1] + def change + add_column :application_settings, :max_decompressed_archive_size, :integer, default: 25600, null: false + end +end diff --git a/db/migrate/20230802070337_add_application_settings_max_decompression_size_constraint.rb b/db/migrate/20230802070337_add_application_settings_max_decompression_size_constraint.rb new file mode 100644 index 00000000000..7698aef6f0b --- /dev/null +++ b/db/migrate/20230802070337_add_application_settings_max_decompression_size_constraint.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddApplicationSettingsMaxDecompressionSizeConstraint < Gitlab::Database::Migration[2.1] + CONSTRAINT_NAME = 'app_settings_max_decompressed_archive_size_positive' + + disable_ddl_transaction! + + def up + add_check_constraint :application_settings, 'max_decompressed_archive_size >= 0', CONSTRAINT_NAME + end + + def down + remove_check_constraint :application_settings, CONSTRAINT_NAME + end +end diff --git a/db/schema_migrations/20230802065830 b/db/schema_migrations/20230802065830 new file mode 100644 index 00000000000..18e459034a1 --- /dev/null +++ b/db/schema_migrations/20230802065830 @@ -0,0 +1 @@ +1bc56bc33ef336f4c97d15c8b726dad319a5775b8f8efa759bb14bb9a4db44eb
\ No newline at end of file diff --git a/db/schema_migrations/20230802070337 b/db/schema_migrations/20230802070337 new file mode 100644 index 00000000000..7defc6bb63d --- /dev/null +++ b/db/schema_migrations/20230802070337 @@ -0,0 +1 @@ +1b158b3191749032084d69f68e02f8aaddeb640ee10db4f1bb43041e4fd2d4ac
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f83499f1442..96900fba8b9 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11861,12 +11861,14 @@ CREATE TABLE application_settings ( bulk_import_max_download_file_size bigint DEFAULT 5120 NOT NULL, max_import_remote_file_size bigint DEFAULT 10240 NOT NULL, protected_paths_for_get_request text[] DEFAULT '{}'::text[] NOT NULL, + max_decompressed_archive_size integer DEFAULT 25600 NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)), CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), CONSTRAINT app_settings_git_rate_limit_users_alertlist_max_usernames CHECK ((cardinality(git_rate_limit_users_alertlist) <= 100)), CONSTRAINT app_settings_git_rate_limit_users_allowlist_max_usernames CHECK ((cardinality(git_rate_limit_users_allowlist) <= 100)), + CONSTRAINT app_settings_max_decompressed_archive_size_positive CHECK ((max_decompressed_archive_size >= 0)), CONSTRAINT app_settings_max_pages_custom_domains_per_project_check CHECK ((max_pages_custom_domains_per_project >= 0)), CONSTRAINT app_settings_max_terraform_state_size_bytes_check CHECK ((max_terraform_state_size_bytes >= 0)), CONSTRAINT app_settings_p_cleanup_package_file_worker_capacity_positive CHECK ((packages_cleanup_package_file_worker_capacity >= 0)), diff --git a/doc/administration/logs/index.md b/doc/administration/logs/index.md index f18fa05416b..60d4ee223ee 100644 --- a/doc/administration/logs/index.md +++ b/doc/administration/logs/index.md @@ -820,6 +820,23 @@ This file is located at: This structured log file records internal activity in the `mail_room` gem. Its name and path are configurable, so the name and path may not match the above. +## `web_hooks.log` + +> Introduced in GitLab 16.3. + +This file is located at: + +- `/var/log/gitlab/gitlab-rails/web_hooks.log` on Linux package installations. +- `/home/git/gitlab/log/web_hooks.log` on self-compiled installations. + +The back-off, disablement, and re-enablement events for Webhook are recorded in this file. For example: + +```json +{"severity":"INFO","time":"2020-11-24T02:30:59.860Z","hook_id":12,"action":"backoff","disabled_until":"2020-11-24T04:30:59.860Z","backoff_count":2,"recent_failures":2} +{"severity":"INFO","time":"2020-11-24T02:30:59.860Z","hook_id":12,"action":"disable","disabled_until":null,"backoff_count":5,"recent_failures":100} +{"severity":"INFO","time":"2020-11-24T02:30:59.860Z","hook_id":12,"action":"enable","disabled_until":null,"backoff_count":0,"recent_failures":0} +``` + ## Reconfigure logs Reconfigure log files are in `/var/log/gitlab/reconfigure` for Linux package installations. Self-compiled installations diff --git a/doc/administration/settings/account_and_limit_settings.md b/doc/administration/settings/account_and_limit_settings.md index 592abe814d9..de0b43f9dda 100644 --- a/doc/administration/settings/account_and_limit_settings.md +++ b/doc/administration/settings/account_and_limit_settings.md @@ -135,6 +135,26 @@ To modify the maximum download file size for imports by direct transfer: 1. Expand **Account and limit**. 1. Increase or decrease by changing the value in **Direct transfer maximum download file size (MB)**. Set to `0` to set no download file size limit. +## Maximum decompressed file size for imported archives + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128218) in GitLab 16.3. + +When you [import a project](../../user/project/settings/import_export.md), you can specify the maximum decompressed file size for imported archives. The default value is 25 GB. + +When you import a compressed file, the decompressed size cannot exceed the maximum decompressed file size limit. If the decompressed size exceeds the configured limit, the following error is returned: + +```plaintext +Decompressed archive size validation failed. +``` + +To modify the maximum decompressed file size for imports in GitLab: + +1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). +1. Select **Admin Area**. +1. Select **Settings > General**. +1. Expand **Account and limit**. +1. Set another value for **Maximum decompressed size (MiB)**. + ## Personal access token prefix > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20968) in GitLab 13.7. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 5b4d8fc1389..09c9d138c3f 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -6888,6 +6888,7 @@ Input type: `UserAddOnAssignmentCreateInput` | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="mutationuseraddonassignmentcreateaddonpurchase"></a>`addOnPurchase` | [`AddOnPurchase`](#addonpurchase) | AddOnPurchase state after mutation. | | <a id="mutationuseraddonassignmentcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <a id="mutationuseraddonassignmentcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | diff --git a/doc/api/packages/nuget.md b/doc/api/packages/nuget.md index cbfd12c7233..075201d14cb 100644 --- a/doc/api/packages/nuget.md +++ b/doc/api/packages/nuget.md @@ -324,7 +324,8 @@ Example response: "version": "1.3.0.17", "tags": "", "packageContent": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg", - "summary": "Summary of the package", + "description": "Description of the package", + "summary": "Description of the package", "published": "2023-05-08T17:23:25Z", } } @@ -367,7 +368,8 @@ Example response: "version": "1.3.0.17", "tags": "", "packageContent": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg", - "summary": "Summary of the package", + "description": "Description of the package", + "summary": "Description of the package", "published": "2023-05-08T17:23:25Z", } } @@ -405,7 +407,8 @@ Example response: "authors": "Author1, Author2", "id": "MyNuGetPkg", "title": "MyNuGetPkg", - "summary": "Summary of the package", + "description": "Description of the package", + "summary": "Description of the package", "totalDownloads": 0, "verified": true, "version": "1.3.0.17", diff --git a/doc/api/settings.md b/doc/api/settings.md index 64f8129a003..03417ac7ebc 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -44,6 +44,7 @@ Example response: "max_export_size": 50, "max_import_size": 50, "max_import_remote_file_size": 10240, + "max_decompressed_archive_size": 25600, "user_oauth_applications" : true, "updated_at" : "2016-01-04T15:44:55.176Z", "session_expire_delay" : 10080, @@ -185,6 +186,7 @@ Example response: "max_export_size": 50, "max_import_size": 50, "max_import_remote_file_size": 10240, + "max_decompressed_archive_size": 25600, "session_expire_delay": 10080, "default_ci_config_path" : null, "default_project_visibility": "internal", @@ -456,9 +458,10 @@ listed in the descriptions of the relevant settings. | `maintenance_mode` **(PREMIUM)** | boolean | no | When instance is in maintenance mode, non-administrative users can sign in with read-only access and make read-only API requests. | | `max_artifacts_size` | integer | no | Maximum artifacts size in MB. | | `max_attachment_size` | integer | no | Limit attachment size in MB. | +| `max_decompressed_archive_size` | integer | no | Maximum decompressed file size for imported archives in MB. Set to `0` for unlimited. Default is `25600`. | | `max_export_size` | integer | no | Maximum export size in MB. 0 for unlimited. Default = 0 (unlimited). | | `max_import_size` | integer | no | Maximum import size in MB. 0 for unlimited. Default = 0 (unlimited). [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50 MB to 0 in GitLab 13.8. | -| `max_import_remote_file_size` | integer | no | Maximum remote file size for imports from external object storages. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/384976) in GitLab 16.3. | +| `max_import_remote_file_size` | integer | no | Maximum remote file size for imports from external object storages. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/384976) in GitLab 16.3. | | `max_pages_size` | integer | no | Maximum size of pages repositories in MB. | | `max_personal_access_token_lifetime` **(ULTIMATE SELF)** | integer | no | Maximum allowable lifetime for access tokens in days. When left blank, default value of 365 is applied. When set, value must be 365 or less. When changed, existing access tokens with an expiration date beyond the maximum allowable lifetime are revoked.| | `max_ssh_key_lifetime` **(ULTIMATE SELF)** | integer | no | Maximum allowable lifetime for SSH keys in days. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6. | diff --git a/doc/development/ai_architecture.md b/doc/development/ai_architecture.md index 16df1fc1b04..0490613b116 100644 --- a/doc/development/ai_architecture.md +++ b/doc/development/ai_architecture.md @@ -15,7 +15,7 @@ AI is moving very quickly, and we need to be able to keep pace with changes in t The following diagram from the [architecture blueprint](../architecture/blueprints/ai_gateway/index.md) shows a simplified view of how the different components in GitLab interact. The abstraction layer helps avoid code duplication within the REST APIs within the `AI API` block. -![architecture diagram](../architecture/blueprints/ai_gateway/img/architecture.png) +![architecture diagram](img/architecture.png) ## SaaS-based AI abstraction layer diff --git a/doc/development/ai_features.md b/doc/development/ai_features.md index 2a0e4d36858..79d03ff6c72 100644 --- a/doc/development/ai_features.md +++ b/doc/development/ai_features.md @@ -1,6 +1,6 @@ --- -stage: none -group: none +stage: ModelOps +group: AI Framework info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- @@ -421,6 +421,52 @@ module EE end ``` +### Pairing requests with responses + +Because multiple users' requests can be processed in parallel, when receiving responses, +it can be difficult to pair a response with its original request. The `requestId` +field can be used for this purpose, because both the request and response are assured +to have the same `requestId` UUID. + +### Caching + +AI requests and responses can be cached. Cached conversation is being used to +display user interaction with AI features. In the current implementation, this cache +is not used to skip consecutive calls to the AI service when a user repeats +their requests. + +```graphql +query { + aiMessages { + nodes { + id + requestId + content + role + errors + timestamp + } + } +} +``` + +This cache is especially useful for chat functionality. For other services, +caching is disabled. (It can be enabled for a service by using `skip_cache: false` +option.) + +Caching has following limitations: + +- Messages are stored in Redis stream. +- There is a single stream of messages per user. This means that all services + currently share the same cache. If needed, this could be extended to multiple + streams per user (after checking with the infrastructure team that Redis can handle + the estimated amount of messages). +- Only the last 50 messages (requests + responses) are kept. +- Expiration time of the stream is 3 days since adding last message. +- User can access only their own messages. There is no authorization on the caching + level, and any authorization (if accessed by not current user) is expected on + the service layer. + ### Check if feature is allowed for this resource based on namespace settings There are two settings allowed on root namespace level that restrict the use of AI features: diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index c15588d2462..b983002dbed 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -1298,6 +1298,12 @@ Use lowercase for **runner managers**. These are a type of runner that can creat Use lowercase for **runner workers**. This is the process created by the runner on the host computing platform to run jobs. See also [GitLab Runner](#gitlab-runner). +## runner authentication token + +Use **runner authentication token** instead of variations like **runner token**, **authentication token**, or **token**. +Runners are assigned runner authentication tokens when they are created, and use them to authenticate with GitLab when +they execute jobs. + ## (s) Do not use **(s)** to make a word optionally plural. It can slow down comprehension. For example: diff --git a/doc/development/img/architecture.png b/doc/development/img/architecture.png Binary files differnew file mode 100644 index 00000000000..dea8b5ddb45 --- /dev/null +++ b/doc/development/img/architecture.png diff --git a/doc/security/index.md b/doc/security/index.md index 1b486ab5feb..0ac2ab17e71 100644 --- a/doc/security/index.md +++ b/doc/security/index.md @@ -24,7 +24,7 @@ type: index - [Proxying images](asset_proxy.md) - [CI/CD variables](../ci/variables/index.md#cicd-variable-security) - [Token overview](token_overview.md) -- [Project Import decompressed archive size limits](project_import_decompressed_archive_size_limits.md) +- [Maximum decompressed file size for imported archives](../administration/settings/account_and_limit_settings.md#maximum-decompressed-file-size-for-imported-archives) - [Responding to security incidents](responding_to_security_incidents.md) To harden your GitLab instance and minimize the risk of unwanted user account creation, consider access control features like [Sign up restrictions](../administration/settings/sign_up_restrictions.md) and [Authentication options](../topics/authentication/index.md). For more detailed information, refer to [Hardening](hardening.md). diff --git a/doc/security/project_import_decompressed_archive_size_limits.md b/doc/security/project_import_decompressed_archive_size_limits.md index 980a084347a..48767740625 100644 --- a/doc/security/project_import_decompressed_archive_size_limits.md +++ b/doc/security/project_import_decompressed_archive_size_limits.md @@ -1,31 +1,11 @@ --- -stage: Manage -group: Authentication and Authorization -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments -type: reference, howto +redirect_to: '../administration/settings/account_and_limit_settings.md#maximum-decompressed-file-size-for-imported-archives' +remove_date: '2023-11-02' --- -# Project import decompressed archive size limits **(FREE SELF)** +This document was moved to [another location](../administration/settings/account_and_limit_settings.md#maximum-decompressed-file-size-for-imported-archives). -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31564) in GitLab 13.2. -> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63025) in GitLab 14.0. - -When using [Project Import](../user/project/settings/import_export.md), the size of the decompressed project archive is limited to 10 Gb. - -If decompressed size exceeds this limit, `Decompressed archive size validation failed` error is returned. - -## Enable/disable size validation - -If you have a project with decompressed size exceeding this limit, -it is possible to disable the validation by turning off the -`validate_import_decompressed_archive_size` feature flag. - -Start a [Rails console](../administration/operations/rails_console.md#starting-a-rails-console-session). - -```ruby -# Disable -Feature.disable(:validate_import_decompressed_archive_size) - -# Enable -Feature.enable(:validate_import_decompressed_archive_size) -``` +<!-- This redirect file can be deleted after <2023-11-02>. --> +<!-- Redirects that point to other docs in the same project expire in three months. --> +<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> diff --git a/doc/security/user_email_confirmation.md b/doc/security/user_email_confirmation.md index f2c0052c2b8..899fed0b584 100644 --- a/doc/security/user_email_confirmation.md +++ b/doc/security/user_email_confirmation.md @@ -21,14 +21,7 @@ they confirm their email address. By default, a user can confirm their account within 24 hours after the confirmation email was sent. After 24 hours, the confirmation token becomes invalid. -<!-- ## Troubleshooting +## Automatically delete unconfirmed users **(PREMIUM SELF)** -Include any troubleshooting steps that you can foresee. If you know beforehand what issues -one might have when setting this up, or when something is changed, or on upgrading, it's -important to describe those, too. Think of things that may go wrong and include them here. -This is important to minimize requests for support, and to avoid doc comments with -questions that you know someone might ask. - -Each scenario can be a third-level heading, for example `### Getting error message X`. -If you have none to add when creating a doc, leave this section in place -but commented out to help encourage others to add to it in the future. --> +When email confirmation is turned on, administrators can enable the setting to +[automatically delete unconfirmed users](../administration/moderate_users.md#automatically-delete-unconfirmed-users). diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index f6008b9b31a..8455042381d 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -212,6 +212,7 @@ the default value [is the same as for self-managed instances](../../administrati | Maximum remote file size for imports from external object storages | 10 GB | | Maximum download file size when importing from source GitLab instances by direct transfer | 5 GB | | Maximum attachment size | 100 MB | +| [Maximum decompressed file size for imported archives](../../administration/settings/account_and_limit_settings.md#maximum-decompressed-file-size-for-imported-archives) | 25 GB | If you are near or over the repository size limit, you can either [reduce your repository size with Git](../project/repository/reducing_the_repo_size_using_git.md) diff --git a/doc/user/group/access_and_permissions.md b/doc/user/group/access_and_permissions.md index 8bebaae003c..fa6130fe56d 100644 --- a/doc/user/group/access_and_permissions.md +++ b/doc/user/group/access_and_permissions.md @@ -101,6 +101,7 @@ Keep in mind that restricting group access by IP address has the following impli - IP access restrictions for Git operations via SSH are supported on GitLab SaaS. IP access restrictions applied to self-managed instances are possible with [`gitlab-sshd`](../../administration/operations/gitlab_sshd.md) with [PROXY protocol](../../administration/operations/gitlab_sshd.md#proxy-protocol-support) enabled. +- IP restriction is not applicable to shared resources belonging to a group. Any shared resource is accessible to a user even if that user is not able to access the group. ## Restrict group access by domain **(PREMIUM)** diff --git a/doc/user/packages/composer_repository/index.md b/doc/user/packages/composer_repository/index.md index 3f8b0761188..60bb6c4f59d 100644 --- a/doc/user/packages/composer_repository/index.md +++ b/doc/user/packages/composer_repository/index.md @@ -38,7 +38,7 @@ Prerequisites: the [Composer specification](https://getcomposer.org/doc/04-schema.md#version). If the version is not valid, for example, it has three dots (`1.0.0.0`), an error (`Validation failed: Version is invalid`) occurs when you publish. -- A valid `composer.json` file. +- A valid `composer.json` file at the project root directory. - The Packages feature is enabled in a GitLab repository. - The project ID, which is on the project's home page. - One of the following token types: @@ -324,6 +324,14 @@ If you committed your `composer.lock`, you could do a `composer install` in CI w In GitLab 14.10 and later, authorization is required for the [downloading a package archive](../../../api/packages/composer.md#download-a-package-archive) endpoint. If you encounter a credentials prompt when you are using `composer install`, follow the instructions in the [install a composer package](#install-a-composer-package) section to create an `auth.json` file. +### Publish fails with `The file composer.json was not found` + +You might see an error that says `The file composer.json was not found`. + +This issue occurs when [configuration requirements for publishing a package](#publish-a-composer-package-by-using-the-api) are not met. + +To resolve the error, commit a `composer.json` file to the project root directory. + ## Supported CLI commands The GitLab Composer repository supports the following Composer CLI commands: diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 6e7dcb7e5fd..a4b7db9361b 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -284,6 +284,7 @@ More details about the permissions for some project-level features follow. | Run CI/CD pipeline | | | | ✓ | ✓ | ✓ | | Run CI/CD pipeline for a protected branch | | | | ✓ (5) | ✓ (5) | ✓ | | Stop [environments](../ci/environments/index.md) | | | | ✓ | ✓ | ✓ | +| Run deployment job for a protected environment | | | ✓ (5) | ✓ (6) | ✓ (6) | ✓ | | View a job with [debug logging](../ci/variables/index.md#enable-debug-logging) | | | | ✓ | ✓ | ✓ | | Use pipeline editor | | | | ✓ | ✓ | ✓ | | Run [interactive web terminals](../ci/interactive_web_terminal/index.md) | | | | ✓ | ✓ | ✓ | @@ -307,6 +308,7 @@ More details about the permissions for some project-level features follow. - [In GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/35069) and later, run for a non-protected branch. 5. If the user is [allowed to merge or push to the protected branch](../ci/pipelines/index.md#pipeline-security-on-protected-branches). +6. If the user if [part of a group with at least the Reporter role](../ci/environments/protected_environments.md#deployment-only-access-to-protected-environments) <!-- markdownlint-enable MD029 --> diff --git a/lib/api/entities/nuget/metadatum.rb b/lib/api/entities/nuget/metadatum.rb index c316dfce740..1df57f8243d 100644 --- a/lib/api/entities/nuget/metadatum.rb +++ b/lib/api/entities/nuget/metadatum.rb @@ -7,8 +7,10 @@ module API expose :authors, documentation: { type: 'string', example: 'Authors' } do |metadatum| metadatum[:authors] || '' end - expose :description, as: :summary, documentation: { type: 'string', example: 'Description' } do |metadatum| - metadatum[:description] || '' + with_options documentation: { type: 'string', example: 'Description' } do + set_default = ->(metadatum) { metadatum[:description] || '' } + expose :description, &set_default + expose :description, as: :summary, &set_default end expose :project_url, as: :projectUrl, expose_nil: false, documentation: { type: 'string', example: 'http://sandbox.com/project' } expose :license_url, as: :licenseUrl, expose_nil: false, documentation: { type: 'string', example: 'http://sandbox.com/license' } diff --git a/lib/api/settings.rb b/lib/api/settings.rb index d7bf6008c20..cc67af1085a 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -106,6 +106,7 @@ module API optional :max_export_size, type: Integer, desc: 'Maximum export size in MB' optional :max_import_size, type: Integer, desc: 'Maximum import size in MB' optional :max_import_remote_file_size, type: Integer, desc: 'Maximum remote file size in MB for imports from external object storages' + optional :max_decompressed_archive_size, type: Integer, desc: 'Maximum decompressed size in MB' optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :max_pages_custom_domains_per_project, type: Integer, desc: 'Maximum number of GitLab Pages custom domains per project' optional :max_terraform_state_size_bytes, type: Integer, desc: "Maximum size in bytes of the Terraform state file. Set this to 0 for unlimited file size." diff --git a/lib/gitlab/ci/config/header/input.rb b/lib/gitlab/ci/config/header/input.rb index 7f0edaaac4c..460adf636f2 100644 --- a/lib/gitlab/ci/config/header/input.rb +++ b/lib/gitlab/ci/config/header/input.rb @@ -11,12 +11,13 @@ module Gitlab include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Attributable - attributes :default, prefix: :input + attributes :default, :type, prefix: :input validations do - validates :config, type: Hash, allowed_keys: [:default] + validates :config, type: Hash, allowed_keys: [:default, :type] validates :key, alphanumeric: true validates :input_default, alphanumeric: true, allow_nil: true + validates :input_type, allow_nil: true, allowed_values: ::Gitlab::Ci::Interpolation::Inputs.input_types end end end diff --git a/lib/gitlab/ci/interpolation/inputs.rb b/lib/gitlab/ci/interpolation/inputs.rb index 910606e6ab2..7a9a5fdfe0e 100644 --- a/lib/gitlab/ci/interpolation/inputs.rb +++ b/lib/gitlab/ci/interpolation/inputs.rb @@ -8,9 +8,15 @@ module Gitlab UnknownInputTypeError = Class.new(StandardError) TYPES = [ + BooleanInput, + NumberInput, StringInput ].freeze + def self.input_types + TYPES.map(&:type_name) + end + def initialize(specs, args) @specs = specs.to_h @args = args.to_h diff --git a/lib/gitlab/ci/interpolation/inputs/base_input.rb b/lib/gitlab/ci/interpolation/inputs/base_input.rb index 20b9bfacb17..cca8b68ad22 100644 --- a/lib/gitlab/ci/interpolation/inputs/base_input.rb +++ b/lib/gitlab/ci/interpolation/inputs/base_input.rb @@ -59,7 +59,7 @@ module Gitlab return error("default value is not a #{self.class.type_name}") if !required_input? && !valid_value?(default) # validate provided value - error("provided value is not a #{self.class.type_name}") unless valid_value?(value) + error("provided value is not a #{self.class.type_name}") unless valid_value?(actual_value) end def error(message) diff --git a/lib/gitlab/ci/interpolation/inputs/boolean_input.rb b/lib/gitlab/ci/interpolation/inputs/boolean_input.rb new file mode 100644 index 00000000000..067da723b14 --- /dev/null +++ b/lib/gitlab/ci/interpolation/inputs/boolean_input.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Interpolation + class Inputs + class BooleanInput < BaseInput + def self.matches?(spec) + spec.is_a?(Hash) && spec[:type] == type_name + end + + def self.type_name + 'boolean' + end + + def valid_value?(value) + [true, false].include?(value) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/interpolation/inputs/number_input.rb b/lib/gitlab/ci/interpolation/inputs/number_input.rb new file mode 100644 index 00000000000..6897e5f766f --- /dev/null +++ b/lib/gitlab/ci/interpolation/inputs/number_input.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Interpolation + class Inputs + class NumberInput < BaseInput + def self.matches?(spec) + spec.is_a?(Hash) && spec[:type] == type_name + end + + def self.type_name + 'number' + end + + def valid_value?(value) + value.is_a?(Numeric) + end + end + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 2fcfa63e726..aac2a212fe4 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -345,6 +345,21 @@ module Gitlab end end + # Return repository recent objects size in mebibytes + # + # This differs from the #size method in that it does not include the size of: + # - stale objects + # - cruft packs of unreachable objects + # + # see: https://gitlab.com/gitlab-org/gitaly/-/blob/257ee33ca268d48c8f99dcbfeaaf7d8b19e07f06/internal/gitaly/service/repository/repository_info.go#L41-62 + def recent_objects_size + wrapped_gitaly_errors do + recent_size_in_bytes = gitaly_repository_client.repository_info.objects.recent_size + + Gitlab::Utils.bytes_to_megabytes(recent_size_in_bytes) + end + end + # Return git object directory size in bytes def object_directory_size gitaly_repository_client.get_object_directory_size.to_f * 1024 diff --git a/lib/gitlab/import_export/decompressed_archive_size_validator.rb b/lib/gitlab/import_export/decompressed_archive_size_validator.rb index 2e39f3f38c2..3609df89958 100644 --- a/lib/gitlab/import_export/decompressed_archive_size_validator.rb +++ b/lib/gitlab/import_export/decompressed_archive_size_validator.rb @@ -5,7 +5,6 @@ module Gitlab class DecompressedArchiveSizeValidator include Gitlab::Utils::StrongMemoize - DEFAULT_MAX_BYTES = 10.gigabytes.freeze TIMEOUT_LIMIT = 210.seconds ServiceError = Class.new(StandardError) @@ -22,7 +21,7 @@ module Gitlab end def self.max_bytes - DEFAULT_MAX_BYTES + Gitlab::CurrentSettings.current_application_settings.max_decompressed_archive_size.megabytes end private @@ -52,7 +51,7 @@ module Gitlab if status.success? result = stdout.readline - if result.to_i > @max_bytes + if @max_bytes > 0 && result.to_i > @max_bytes valid_archive = false log_error('Decompressed archive size limit reached') diff --git a/lib/gitlab/metrics/dashboard/service_selector.rb b/lib/gitlab/metrics/dashboard/service_selector.rb index 3ccd28c1e72..76ff00f86d8 100644 --- a/lib/gitlab/metrics/dashboard/service_selector.rb +++ b/lib/gitlab/metrics/dashboard/service_selector.rb @@ -13,7 +13,6 @@ module Gitlab include Gitlab::Utils::StrongMemoize SERVICES = [ - ::Metrics::Dashboard::DefaultEmbedService, ::Metrics::Dashboard::SystemDashboardService ].freeze diff --git a/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter.rb b/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter.rb deleted file mode 100644 index b3ce0b79675..00000000000 --- a/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Metrics - module Dashboard - module Stages - class VariableEndpointInserter < BaseStage - VARIABLE_TYPE_METRIC_LABEL_VALUES = 'metric_label_values' - - def transform! - raise Errors::DashboardProcessingError, _('Environment is required for Stages::VariableEndpointInserter') unless params[:environment] - - for_variables do |variable_name, variable| - if variable.is_a?(Hash) && variable[:type] == VARIABLE_TYPE_METRIC_LABEL_VALUES - variable[:options][:prometheus_endpoint_path] = endpoint_for_variable(variable.dig(:options, :series_selector)) - end - end - end - - private - - def endpoint_for_variable(series_selector) - Gitlab::Routing.url_helpers.prometheus_api_project_environment_path( - project, - params[:environment], - proxy_path: ::Prometheus::ProxyService::PROMETHEUS_SERIES_API, - match: Array(series_selector) - ) - end - end - end - end - end -end diff --git a/lib/gitlab/web_hooks/logger.rb b/lib/gitlab/web_hooks/logger.rb new file mode 100644 index 00000000000..010e40a3dab --- /dev/null +++ b/lib/gitlab/web_hooks/logger.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Gitlab + module WebHooks + class Logger < ::Gitlab::JsonLogger + def self.file_name_noext + 'web_hooks' + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bc18812ad39..758f6701083 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -17904,9 +17904,6 @@ msgstr "" msgid "Environment" msgstr "" -msgid "Environment is required for Stages::VariableEndpointInserter" -msgstr "" - msgid "Environment scope" msgstr "" @@ -23984,12 +23981,18 @@ msgstr "" msgid "Import|GitHub import details" msgstr "" +msgid "Import|Maximum decompressed size (MiB)" +msgstr "" + msgid "Import|Maximum import remote file size (MB)" msgstr "" msgid "Import|Maximum remote file size for imports from external object storages. For example, AWS S3." msgstr "" +msgid "Import|Maximum size of decompressed archive." +msgstr "" + msgid "Import|No import details" msgstr "" @@ -53844,6 +53847,9 @@ msgstr "" msgid "You must be logged in to search across all of GitLab" msgstr "" +msgid "You must confirm your email within %{cut_off_days} days of signing up. If you do not confirm your email in this timeframe, your account will be deleted and you will need to sign up for GitLab again." +msgstr "" + msgid "You must have developer or higher permissions in the associated project to view job logs when debug trace is enabled. To disable debug trace, set the 'CI_DEBUG_TRACE' and 'CI_DEBUG_SERVICES' variables to 'false' in your pipeline configuration or CI/CD settings. If you must view this job log, a project maintainer or owner must add you to the project with developer permissions or higher." msgstr "" diff --git a/package.json b/package.json index ba96261a41a..b9120d6188a 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@gitlab/svgs": "3.58.0", "@gitlab/ui": "64.20.1", "@gitlab/visual-review-tools": "1.7.3", - "@gitlab/web-ide": "0.0.1-dev-20230713160749-patch-1", + "@gitlab/web-ide": "0.0.1-dev-20230802205337", "@mattiasbuelens/web-streams-adapter": "^0.1.0", "@popperjs/core": "^2.11.2", "@rails/actioncable": "7.0.6", diff --git a/spec/channels/noteable/notes_channel_spec.rb b/spec/channels/noteable/notes_channel_spec.rb new file mode 100644 index 00000000000..a38155e7ffd --- /dev/null +++ b/spec/channels/noteable/notes_channel_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Noteable::NotesChannel, feature_category: :team_planning do + let_it_be(:project) { create(:project, :repository, :private) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + + describe '#subscribed' do + let(:subscribe_params) do + { + project_id: noteable.project_id, + noteable_type: noteable.class.underscore, + noteable_id: noteable.id + } + end + + before do + stub_action_cable_connection current_user: developer + end + + it 'rejects the subscription when noteable params are missing' do + subscribe(project_id: project.id) + + expect(subscription).to be_rejected + end + + context 'on an issue' do + let_it_be(:noteable) { create(:issue, project: project) } + + it_behaves_like 'handle subscription based on user access' + end + + context 'on a merge request' do + let_it_be(:noteable) { create(:merge_request, source_project: project) } + + it_behaves_like 'handle subscription based on user access' + end + end +end diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 23f9347d726..a390dca6822 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -10,17 +10,31 @@ RSpec.describe 'Issue notes polling', :js, feature_category: :team_planning do let(:issue) { create(:issue, project: project) } describe 'creates' do - before do + it 'displays the new comment' do visit project_issue_path(project, issue) close_rich_text_promo_popover_if_present - end - it 'displays the new comment' do note = create(:note, noteable: issue, project: project, note: 'Looks good!') wait_for_requests expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') end + + context 'when action_cable_notes is disabled' do + before do + stub_feature_flags(action_cable_notes: false) + end + + it 'displays the new comment' do + visit project_issue_path(project, issue) + close_rich_text_promo_popover_if_present + + note = create(:note, noteable: issue, project: project, note: 'Looks good!') + wait_for_requests + + expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') + end + end end describe 'updates' do diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json index 9c80f1621ad..c27ea2840a7 100644 --- a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json +++ b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json @@ -20,6 +20,7 @@ "dependencyGroups", "id", "packageContent", + "description", "summary", "version" ], @@ -36,6 +37,9 @@ "packageContent": { "type": "string" }, + "description": { + "type": "string" + }, "summary": { "type": "string" }, diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json index 94b5ad48a1e..d17a3176433 100644 --- a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json +++ b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json @@ -49,6 +49,7 @@ "dependencyGroups", "id", "packageContent", + "description", "summary", "version" ], @@ -65,6 +66,9 @@ "packageContent": { "type": "string" }, + "description": { + "type": "string" + }, "summary": { "type": "string" }, diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json index 41ad7379d73..74f2913e5b9 100644 --- a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json +++ b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json @@ -16,6 +16,7 @@ "@type", "authors", "id", + "description", "summary", "title", "totalDownloads", @@ -32,6 +33,9 @@ "id": { "type": "string" }, + "description": { + "type": "string" + }, "summary": { "type": "string" }, diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index f35b25fdf04..450eeefd898 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -421,6 +421,16 @@ describe('URL utility', () => { window.location = originalLocation; }); + it.each` + inputQuery | expectedQuery + ${'?scope=all&state=merged'} | ${'?scope=all&state=merged'} + ${'?'} | ${'?'} + `('handles query string: $inputQuery', ({ inputQuery, expectedQuery }) => { + window.location.href = mockUrl; + urlUtils.visitUrl(inputQuery); + expect(window.location.assign).toHaveBeenCalledWith(`${mockUrl}${expectedQuery}`); + }); + it('does not navigate to unsafe urls', () => { // eslint-disable-next-line no-script-url const url = 'javascript:alert(document.domain)'; diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js index 94549c4a73b..b291eba61f5 100644 --- a/spec/frontend/notes/mock_data.js +++ b/spec/frontend/notes/mock_data.js @@ -15,6 +15,10 @@ export const notesDataMock = { closePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close', reopenPath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen', canAwardEmoji: true, + noteableType: 'issue', + noteableId: 1, + projectId: 2, + groupId: null, }; export const userDataMock = { diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js index 50df63d06af..0205f606297 100644 --- a/spec/frontend/notes/stores/actions_spec.js +++ b/spec/frontend/notes/stores/actions_spec.js @@ -2,6 +2,7 @@ import AxiosMockAdapter from 'axios-mock-adapter'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import testAction from 'helpers/vuex_action_helper'; import { TEST_HOST } from 'spec/test_constants'; +import actionCable from '~/actioncable_consumer'; import Api from '~/api'; import { createAlert } from '~/alert'; import toast from '~/vue_shared/plugins/global_toast'; @@ -44,6 +45,15 @@ jest.mock('~/alert', () => ({ jest.mock('~/vue_shared/plugins/global_toast'); +jest.mock('@rails/actioncable', () => { + const mockConsumer = { + subscriptions: { create: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }) }, + }; + return { + createConsumer: jest.fn().mockReturnValue(mockConsumer), + }; +}); + describe('Actions Notes Store', () => { let commit; let dispatch; @@ -251,6 +261,59 @@ describe('Actions Notes Store', () => { }); }); + describe('initPolling', () => { + afterEach(() => { + gon.features = {}; + }); + + it('creates the Action Cable subscription', () => { + gon.features = { actionCableNotes: true }; + + store.dispatch('setNotesData', notesDataMock); + store.dispatch('initPolling'); + + expect(actionCable.subscriptions.create).toHaveBeenCalledTimes(1); + expect(actionCable.subscriptions.create).toHaveBeenCalledWith( + { + channel: 'Noteable::NotesChannel', + project_id: store.state.notesData.projectId, + group_id: store.state.notesData.groupId, + noteable_type: store.state.notesData.noteableType, + noteable_id: store.state.notesData.noteableId, + }, + expect.any(Object), + ); + }); + }); + + describe('fetchUpdatedNotes', () => { + const response = { notes: [], last_fetched_at: '123456' }; + const successMock = () => + axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_OK, response); + const failureMock = () => + axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); + + beforeEach(() => { + return store.dispatch('setNotesData', notesDataMock); + }); + + it('calls the endpoint and stores last fetched state', async () => { + successMock(); + + await store.dispatch('fetchUpdatedNotes'); + + expect(store.state.lastFetchedAt).toBe('123456'); + }); + + it('shows an alert when fetching fails', async () => { + failureMock(); + + await store.dispatch('fetchUpdatedNotes'); + + expect(createAlert).toHaveBeenCalledTimes(1); + }); + }); + describe('poll', () => { const pollInterval = 6000; const pollResponse = { notes: [], last_fetched_at: '123456' }; diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 91635ffcdc0..62c0d1b1ff7 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -331,7 +331,9 @@ RSpec.describe NotesHelper, feature_category: :team_planning do end describe '#notes_data' do - let(:issue) { create(:issue, project: project) } + let_it_be(:issue) { create(:issue, project: project) } + + let(:notes_data) { helper.notes_data(issue) } before do @project = project @@ -343,7 +345,14 @@ RSpec.describe NotesHelper, feature_category: :team_planning do it 'includes the current notes filter for the user' do guest.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issue) - expect(helper.notes_data(issue)[:notesFilter]).to eq(UserPreference::NOTES_FILTERS[:only_comments]) + expect(notes_data[:notesFilter]).to eq(UserPreference::NOTES_FILTERS[:only_comments]) + end + + it 'includes info about the noteable', :aggregate_failures do + expect(notes_data[:noteableType]).to eq('issue') + expect(notes_data[:noteableId]).to eq(issue.id) + expect(notes_data[:projectId]).to eq(project.id) + expect(notes_data[:groupId]).to be_nil end end end diff --git a/spec/lib/api/entities/nuget/metadatum_spec.rb b/spec/lib/api/entities/nuget/metadatum_spec.rb index cb4e53a1960..2cf26d59279 100644 --- a/spec/lib/api/entities/nuget/metadatum_spec.rb +++ b/spec/lib/api/entities/nuget/metadatum_spec.rb @@ -16,6 +16,7 @@ RSpec.describe API::Entities::Nuget::Metadatum, feature_category: :package_regis let(:expected) do { 'authors': 'Authors', + 'description': 'Description', 'summary': 'Description', 'projectUrl': 'http://sandbox.com/project', 'licenseUrl': 'http://sandbox.com/license', @@ -50,8 +51,10 @@ RSpec.describe API::Entities::Nuget::Metadatum, feature_category: :package_regis context 'with default value' do let(:metadatum) { super().merge(description: nil) } + it { is_expected.to have_key(:description) } it { is_expected.to have_key(:summary) } - it { is_expected.to eq(expected.merge(summary: '')) } + + it { is_expected.to eq(expected.merge(description: '', summary: '')) } end end end diff --git a/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb b/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb index 2fad42f907b..b39456973ea 100644 --- a/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb +++ b/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb @@ -31,6 +31,7 @@ RSpec.describe API::Entities::Nuget::PackageMetadataCatalogEntry, feature_catego 'dependencyGroups': [], 'tags': 'tag1 tag2 tag3', 'packageContent': 'http://sandbox.com/archive/package', + 'description': 'Summary', 'summary': 'Summary', 'projectUrl': 'http://sandbox.com/project', 'licenseUrl': 'http://sandbox.com/license', diff --git a/spec/lib/api/entities/nuget/search_result_spec.rb b/spec/lib/api/entities/nuget/search_result_spec.rb index 5edff28824f..9de2719999e 100644 --- a/spec/lib/api/entities/nuget/search_result_spec.rb +++ b/spec/lib/api/entities/nuget/search_result_spec.rb @@ -34,6 +34,7 @@ RSpec.describe API::Entities::Nuget::SearchResult, feature_category: :package_re 'authors': 'Author', 'id': 'PackageTest', 'title': 'PackageTest', + 'description': 'Description', 'summary': 'Description', 'totalDownloads': 100, 'verified': true, diff --git a/spec/lib/gitlab/ci/config/header/input_spec.rb b/spec/lib/gitlab/ci/config/header/input_spec.rb index 73b5b8f9497..e3efe4b3e18 100644 --- a/spec/lib/gitlab/ci/config/header/input_spec.rb +++ b/spec/lib/gitlab/ci/config/header/input_spec.rb @@ -46,12 +46,29 @@ RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_co it_behaves_like 'a valid input' end - context 'when is a required required input' do + context 'when is a required input' do let(:input_hash) { nil } it_behaves_like 'a valid input' end + context 'when given a valid type' do + where(:input_type) { ::Gitlab::Ci::Interpolation::Inputs.input_types } + + with_them do + let(:input_hash) { { type: input_type } } + + it_behaves_like 'a valid input' + end + end + + context 'when given an invalid type' do + let(:input_hash) { { type: 'datetime' } } + let(:expected_errors) { ['foo input type unknown value: datetime'] } + + it_behaves_like 'an invalid input' + end + context 'when contains unknown keywords' do let(:input_hash) { { test: 123 } } let(:expected_errors) { ['foo config contains unknown keys: test'] } diff --git a/spec/lib/gitlab/ci/interpolation/inputs_spec.rb b/spec/lib/gitlab/ci/interpolation/inputs_spec.rb index d7d7c85d04f..562fbd41161 100644 --- a/spec/lib/gitlab/ci/interpolation/inputs_spec.rb +++ b/spec/lib/gitlab/ci/interpolation/inputs_spec.rb @@ -37,6 +37,26 @@ RSpec.describe Gitlab::Ci::Interpolation::Inputs, feature_category: :pipeline_co [ { foo: { default: 'bar' }, baz: nil }, { baz: 'test' }, { foo: 'bar', baz: 'test' } + ], + [ + { number_input: { type: 'number' } }, + { number_input: 8 }, + { number_input: 8 } + ], + [ + { default_number_input: { default: 9, type: 'number' } }, + {}, + { default_number_input: 9 } + ], + [ + { true_input: { type: 'boolean' }, false_input: { type: 'boolean' } }, + { true_input: true, false_input: false }, + { true_input: true, false_input: false } + ], + [ + { default_boolean_input: { default: true, type: 'boolean' } }, + {}, + { default_boolean_input: true } ] ] end @@ -62,11 +82,11 @@ RSpec.describe Gitlab::Ci::Interpolation::Inputs, feature_category: :pipeline_co ], [ { foo: 123 }, {}, - ['unknown input specification for `foo` (valid types: string)'] + ['unknown input specification for `foo` (valid types: boolean, number, string)'] ], [ { a: nil, foo: 123 }, { a: '123' }, - ['unknown input specification for `foo` (valid types: string)'] + ['unknown input specification for `foo` (valid types: boolean, number, string)'] ], [ { foo: nil }, {}, @@ -83,6 +103,26 @@ RSpec.describe Gitlab::Ci::Interpolation::Inputs, feature_category: :pipeline_co [ { foo: nil }, { foo: 123 }, ['`foo` input: provided value is not a string'] + ], + [ + { number_input: { type: 'number' } }, + { number_input: 'NaN' }, + ['`number_input` input: provided value is not a number'] + ], + [ + { default_number_input: { default: 'NaN', type: 'number' } }, + {}, + ['`default_number_input` input: default value is not a number'] + ], + [ + { boolean_input: { type: 'boolean' } }, + { boolean_input: 'string' }, + ['`boolean_input` input: provided value is not a boolean'] + ], + [ + { default_boolean_input: { default: 'string', type: 'boolean' } }, + {}, + ['`default_boolean_input` input: default value is not a boolean'] ] ] end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index a97c3c85971..7e55b0c2ee0 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -309,6 +309,32 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen end end + describe '#recent_objects_size' do + subject(:recent_objects_size) { repository.recent_objects_size } + + it { is_expected.to be_a(Float) } + + it 'uses repository_info for size' do + expect(repository.gitaly_repository_client).to receive(:repository_info).and_call_original + + recent_objects_size + end + + it 'returns the recent objects size' do + objects_response = Gitaly::RepositoryInfoResponse::ObjectsInfo.new(recent_size: 5.megabytes) + + allow(repository.gitaly_repository_client).to receive(:repository_info).and_return( + Gitaly::RepositoryInfoResponse.new(objects: objects_response) + ) + + expect(recent_objects_size).to eq 5.0 + end + + it_behaves_like 'wrapping gRPC errors', Gitaly::RepositoryInfoResponse::ObjectsInfo, :recent_size do + subject { recent_objects_size } + end + end + describe '#to_s' do subject { repository.to_s } diff --git a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb index 19f5bdff41e..aceea70be92 100644 --- a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb +++ b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb @@ -47,6 +47,25 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator, feature_c end end + context 'when max_decompressed_archive_size is set to 0' do + subject { described_class.new(archive_path: filepath) } + + before do + stub_application_setting(max_decompressed_archive_size: 0) + end + + it 'is valid and does not log error message' do + expect(Gitlab::Import::Logger) + .not_to receive(:info) + .with( + import_upload_archive_path: filepath, + import_upload_archive_size: File.size(filepath), + message: 'Decompressed archive size limit reached' + ) + expect(subject.valid?).to eq(true) + end + end + context 'when exception occurs during decompression' do shared_examples 'logs raised exception and terminates validator process group' do let(:std) { double(:std, close: nil, value: nil) } diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb index 6fc9e05037c..e94816c102f 100644 --- a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb @@ -34,50 +34,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store it_behaves_like 'valid dashboard service response' end - - context 'when the dashboard is expected to be embedded' do - let(:service_call) { described_class.find(project, user, **params) } - let(:params) { { environment: environment, embedded: true } } - - it_behaves_like 'valid embedded dashboard service response' - - context 'when params are incomplete' do - let(:params) { { environment: environment, embedded: true, dashboard_path: system_dashboard_path } } - - it_behaves_like 'valid embedded dashboard service response' - end - - context 'when the panel is specified' do - context 'as a custom metric' do - let(:params) do - { - environment: environment, - embedded: true, - dashboard_path: system_dashboard_path, - group: business_metric_title, - title: 'title', - y_label: 'y_label' - } - end - - context 'when the metric exists' do - before do - create(:prometheus_metric, project: project) - end - - it_behaves_like 'valid embedded dashboard service response' - end - end - - context 'as a project-defined panel' do - context 'when the metric exists' do - let(:project) { project_with_dashboard(dashboard_path) } - - it_behaves_like 'valid embedded dashboard service response' - end - end - end - end end describe '.find_raw' do diff --git a/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb b/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb index b9ed89d9ee7..cabd453cc3a 100644 --- a/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb @@ -19,17 +19,5 @@ RSpec.describe Gitlab::Metrics::Dashboard::ServiceSelector do it { is_expected.to be Metrics::Dashboard::SystemDashboardService } end end - - context 'when the embedded flag is provided' do - let(:arguments) { { embedded: true } } - - it { is_expected.to be Metrics::Dashboard::DefaultEmbedService } - - context 'when an incomplete set of dashboard identifiers are provided' do - let(:arguments) { { embedded: true, dashboard_path: '.gitlab/dashboards/test.yml' } } - - it { is_expected.to be Metrics::Dashboard::DefaultEmbedService } - end - end end end diff --git a/spec/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter_spec.rb deleted file mode 100644 index 9303ff981fb..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/stages/variable_endpoint_inserter_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Dashboard::Stages::VariableEndpointInserter do - include MetricsDashboardHelpers - - let(:project) { build_stubbed(:project) } - let(:environment) { build_stubbed(:environment, project: project) } - - describe '#transform!' do - subject(:transform!) { described_class.new(project, dashboard, environment: environment).transform! } - - let(:dashboard) { load_sample_dashboard.deep_symbolize_keys } - - context 'when dashboard variables are present' do - it 'assigns prometheus_endpoint_path to metric_label_values variable type' do - endpoint_path = Gitlab::Routing.url_helpers.prometheus_api_project_environment_path( - project, - environment, - proxy_path: :series, - match: ['backend:haproxy_backend_availability:ratio{env="{{env}}"}'] - ) - - transform! - - expect( - dashboard.dig(:templating, :variables, :metric_label_values_variable, :options) - ).to include(prometheus_endpoint_path: endpoint_path) - end - - it 'does not modify other variable types' do - original_text_variable = dashboard[:templating][:variables][:text_variable_full_syntax].deep_dup - - transform! - - expect(dashboard[:templating][:variables][:text_variable_full_syntax]).to eq(original_text_variable) - end - - context 'when variable does not have the required series_selector' do - it 'adds prometheus_endpoint_path without match parameter' do - dashboard[:templating][:variables][:metric_label_values_variable][:options].delete(:series_selector) - endpoint_path = Gitlab::Routing.url_helpers.prometheus_api_project_environment_path( - project, - environment, - proxy_path: :series - ) - - transform! - - expect( - dashboard.dig(:templating, :variables, :metric_label_values_variable, :options) - ).to include(prometheus_endpoint_path: endpoint_path) - end - end - end - - context 'when no variables are present' do - it 'does not fail' do - dashboard.delete(:templating) - - expect { transform! }.not_to raise_error - end - end - - context 'with no environment' do - subject(:transform!) { described_class.new(project, dashboard, {}).transform! } - - it 'raises error' do - expect { transform! }.to raise_error( - Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError, - 'Environment is required for Stages::VariableEndpointInserter' - ) - end - end - end -end diff --git a/spec/mailers/devise_mailer_spec.rb b/spec/mailers/devise_mailer_spec.rb index 23ad20f3ad6..0a6d38996b7 100644 --- a/spec/mailers/devise_mailer_spec.rb +++ b/spec/mailers/devise_mailer_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require 'email_spec' -RSpec.describe DeviseMailer do +RSpec.describe DeviseMailer, feature_category: :user_management do include EmailSpec::Matchers include_context 'gitlab email notification' diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index bc3974fb1ee..b2275be5893 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -24,6 +24,7 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do it { expect(setting.repository_storages_weighted).to eq({}) } it { expect(setting.kroki_formats).to eq({}) } it { expect(setting.default_branch_protection_defaults).to eq({}) } + it { expect(setting.max_decompressed_archive_size).to eq(25600) } end describe 'validations' do @@ -602,6 +603,14 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do .is_greater_than_or_equal_to(0) end + it { is_expected.to validate_presence_of(:max_decompressed_archive_size) } + + specify do + is_expected.to validate_numericality_of(:max_decompressed_archive_size) + .only_integer + .is_greater_than_or_equal_to(0) + end + specify do is_expected.to validate_numericality_of(:local_markdown_version) .only_integer diff --git a/spec/models/concerns/integrations/reset_secret_fields_spec.rb b/spec/models/concerns/integrations/reset_secret_fields_spec.rb index 3b15b95fea9..411c79624de 100644 --- a/spec/models/concerns/integrations/reset_secret_fields_spec.rb +++ b/spec/models/concerns/integrations/reset_secret_fields_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' -RSpec.describe Integrations::ResetSecretFields do +RSpec.describe Integrations::ResetSecretFields, feature_category: :integrations do let(:described_class) do Class.new(Integration) do - field :username, type: 'text' - field :url, type: 'text', exposes_secrets: true - field :api_url, type: 'text', exposes_secrets: true - field :password, type: 'password' - field :token, type: 'password' + field :username, type: :text + field :url, type: :text, exposes_secrets: true + field :api_url, type: :text, exposes_secrets: true + field :password, type: :password + field :token, type: :password end end diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb index 7fcd74cd37f..ffe1e06a063 100644 --- a/spec/models/integration_spec.rb +++ b/spec/models/integration_spec.rb @@ -856,20 +856,20 @@ RSpec.describe Integration, feature_category: :integrations do Class.new(Integration) do def fields [ - { name: 'token', type: 'password' }, - { name: 'api_token', type: 'password' }, - { name: 'token_api', type: 'password' }, - { name: 'safe_token', type: 'password' }, - { name: 'key', type: 'password' }, - { name: 'api_key', type: 'password' }, - { name: 'password', type: 'password' }, - { name: 'password_field', type: 'password' }, + { name: 'token', type: :password }, + { name: 'api_token', type: :password }, + { name: 'token_api', type: :password }, + { name: 'safe_token', type: :password }, + { name: 'key', type: :password }, + { name: 'api_key', type: :password }, + { name: 'password', type: :password }, + { name: 'password_field', type: :password }, { name: 'webhook' }, { name: 'some_safe_field' }, { name: 'safe_field' }, { name: 'url' }, - { name: 'trojan_horse', type: 'password' }, - { name: 'trojan_gift', type: 'text' }, + { name: 'trojan_horse', type: :password }, + { name: 'trojan_gift', type: :text }, { name: 'api_only_field', api_only: true } ].shuffle end @@ -884,20 +884,20 @@ RSpec.describe Integration, feature_category: :integrations do context 'when the class uses the field DSL' do let(:fake_integration) do Class.new(described_class) do - field :token, type: 'password' - field :api_token, type: 'password' - field :token_api, type: 'password' - field :safe_token, type: 'password' - field :key, type: 'password' - field :api_key, type: 'password' - field :password, type: 'password' - field :password_field, type: 'password' + field :token, type: :password + field :api_token, type: :password + field :token_api, type: :password + field :safe_token, type: :password + field :key, type: :password + field :api_key, type: :password + field :password, type: :password + field :password_field, type: :password field :webhook field :some_safe_field field :safe_field field :url - field :trojan_horse, type: 'password' - field :trojan_gift, type: 'text' + field :trojan_horse, type: :password + field :trojan_gift, type: :text field :api_only_field, api_only: true end end @@ -1030,9 +1030,9 @@ RSpec.describe Integration, feature_category: :integrations do it 'returns all fields with type `password`' do allow(subject).to receive(:fields).and_return( [ - { name: 'password', type: 'password' }, - { name: 'secret', type: 'password' }, - { name: 'public', type: 'text' } + { name: 'password', type: :password }, + { name: 'secret', type: :password }, + { name: 'public', type: :text } ]) expect(subject.secret_fields).to match_array(%w[password secret]) @@ -1117,14 +1117,14 @@ RSpec.describe Integration, feature_category: :integrations do field :foo_p, storage: :properties field :foo_dt, storage: :data_fields - field :bar, type: 'password' + field :bar, type: :password field :password, is_secret: true field :webhook field :with_help, help: -> { 'help' } - field :select, type: 'select' - field :boolean, type: 'checkbox' + field :select, type: :select + field :boolean, type: :checkbox end end @@ -1182,15 +1182,15 @@ RSpec.describe Integration, feature_category: :integrations do specify 'fields have expected attributes' do expect(integration.fields).to include( - have_attributes(name: 'foo', type: 'text'), - have_attributes(name: 'foo_p', type: 'text'), - have_attributes(name: 'foo_dt', type: 'text'), - have_attributes(name: 'bar', type: 'password'), - have_attributes(name: 'password', type: 'password'), - have_attributes(name: 'webhook', type: 'text'), + have_attributes(name: 'foo', type: :text), + have_attributes(name: 'foo_p', type: :text), + have_attributes(name: 'foo_dt', type: :text), + have_attributes(name: 'bar', type: :password), + have_attributes(name: 'password', type: :password), + have_attributes(name: 'webhook', type: :text), have_attributes(name: 'with_help', help: 'help'), - have_attributes(name: 'select', type: 'select'), - have_attributes(name: 'boolean', type: 'checkbox') + have_attributes(name: 'select', type: :select), + have_attributes(name: 'boolean', type: :checkbox) ) end end @@ -1242,7 +1242,7 @@ RSpec.describe Integration, feature_category: :integrations do context 'when using data fields' do let(:klass) do Class.new(Integration) do - field :project_url, storage: :data_fields, type: 'checkbox' + field :project_url, storage: :data_fields, type: :checkbox def data_fields issue_tracker_data || self.build_issue_tracker_data diff --git a/spec/models/integrations/every_integration_spec.rb b/spec/models/integrations/every_integration_spec.rb index c39a3486eb4..9639961c741 100644 --- a/spec/models/integrations/every_integration_spec.rb +++ b/spec/models/integrations/every_integration_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Every integration' do +RSpec.describe 'Every integration', feature_category: :integrations do all_integration_names = Integration.available_integration_names all_integration_names.each do |integration_name| @@ -15,14 +15,14 @@ RSpec.describe 'Every integration' do integration.fields.each do |field| next unless field[:is_secret] - expect(field[:type]).to eq('password'), + expect(field[:type]).to eq(:password), "Field '#{field[:name]}' should use type 'password'" end end it 'defines non-empty titles and help texts for all secret fields' do integration.fields.each do |field| - next unless field[:type] == 'password' + next unless field[:type] == :password expect(field[:non_empty_password_title]).to be_present, "Field '#{field[:name]}' should define :non_empty_password_title" diff --git a/spec/models/integrations/field_spec.rb b/spec/models/integrations/field_spec.rb index ca71dd0e6d3..9017d396c29 100644 --- a/spec/models/integrations/field_spec.rb +++ b/spec/models/integrations/field_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ::Integrations::Field do +RSpec.describe ::Integrations::Field, feature_category: :integrations do subject(:field) { described_class.new(**attrs) } let(:attrs) { { name: nil, integration_class: test_integration } } @@ -17,31 +17,31 @@ RSpec.describe ::Integrations::Field do describe '#initialize' do it 'sets type password for secret fields' do attrs[:is_secret] = true - attrs[:type] = 'text' + attrs[:type] = :text - expect(field[:type]).to eq('password') + expect(field[:type]).to eq(:password) end it 'uses the given type for other names' do attrs[:name] = 'field' - attrs[:type] = 'select' + attrs[:type] = :select - expect(field[:type]).to eq('select') + expect(field[:type]).to eq(:select) end it 'raises an error if an invalid attribute is given' do attrs[:foo] = 'foo' attrs[:bar] = 'bar' attrs[:name] = 'name' - attrs[:type] = 'text' + attrs[:type] = :text expect { field }.to raise_error(ArgumentError, "Invalid attributes [:foo, :bar]") end it 'raises an error if an invalid type is given' do - attrs[:type] = 'other' + attrs[:type] = :other - expect { field }.to raise_error(ArgumentError, 'Invalid type "other"') + expect { field }.to raise_error(ArgumentError, 'Invalid type :other') end end @@ -82,7 +82,7 @@ RSpec.describe ::Integrations::Field do when :api_only be false when :type - eq 'text' + eq :text when :is_secret eq false else @@ -169,7 +169,7 @@ RSpec.describe ::Integrations::Field do context 'when a secret field' do before do - attrs[:type] = 'password' + attrs[:type] = :password end it { is_expected.to be_secret } diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index d56bc210d82..0fc689b9f6c 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -1606,6 +1606,24 @@ RSpec.describe Note, feature_category: :team_planning do .with("/#{noteable.project.namespace.to_param}/#{noteable.project.to_param}/noteable/#{noteable.class.name.underscore}/#{noteable.id}/notes") end + it 'broadcasts an Action Cable event for the noteable' do + expect(Noteable::NotesChannel).to receive(:broadcast_to).with(note.noteable, event: 'updated') + + note.save! + end + + context 'when action_cable_notes is disabled' do + before do + stub_feature_flags(action_cable_notes: false) + end + + it 'does not broadcast an Action Cable event' do + expect(Noteable::NotesChannel).not_to receive(:broadcast_to) + + note.save! + end + end + it "expires cache for note's issue when note is saved" do expect_expiration(note.noteable) diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb index dc4922d8114..2ba7f5c4ca4 100644 --- a/spec/models/project_authorization_spec.rb +++ b/spec/models/project_authorization_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ProjectAuthorization do +RSpec.describe ProjectAuthorization, feature_category: :groups_and_projects do describe 'unique user, project authorizations' do let_it_be(:user) { create(:user) } let_it_be(:project_1) { create(:project) } @@ -85,231 +85,4 @@ RSpec.describe ProjectAuthorization do expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(attributes.map(&:values)) end end - - shared_examples_for 'does not log any detail' do - it 'does not log any detail' do - expect(Gitlab::AppLogger).not_to receive(:info) - - execute - end - end - - shared_examples_for 'logs the detail' do |batch_size:| - it 'logs the detail' do - expect(Gitlab::AppLogger).to receive(:info).with( - entire_size: 3, - message: 'Project authorizations refresh performed with delay', - total_delay: (3 / batch_size.to_f).ceil * ProjectAuthorization::SLEEP_DELAY, - **Gitlab::ApplicationContext.current - ) - - execute - end - end - - describe '.insert_all_in_batches' do - let_it_be(:user) { create(:user) } - let_it_be(:project_1) { create(:project) } - let_it_be(:project_2) { create(:project) } - let_it_be(:project_3) { create(:project) } - - let(:attributes) do - [ - { user_id: user.id, project_id: project_1.id, access_level: Gitlab::Access::MAINTAINER }, - { user_id: user.id, project_id: project_2.id, access_level: Gitlab::Access::MAINTAINER }, - { user_id: user.id, project_id: project_3.id, access_level: Gitlab::Access::MAINTAINER } - ] - end - - subject(:execute) { described_class.insert_all_in_batches(attributes, per_batch_size) } - - before do - # Configure as if a replica database is enabled - allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false) - end - - shared_examples_for 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch' do - specify do - expect(described_class).not_to receive(:sleep) - - execute - - expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(attributes.map(&:values)) - end - end - - context 'when the total number of records to be inserted is greater than the batch size' do - let(:per_batch_size) { 2 } - - it 'inserts the rows in batches, as per the `per_batch` size, with a delay between each batch' do - expect(described_class).to receive(:insert_all).twice.and_call_original - expect(described_class).to receive(:sleep).twice - - execute - - expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(attributes.map(&:values)) - end - - it_behaves_like 'logs the detail', batch_size: 2 - - context 'when the GitLab installation does not have a replica database configured' do - before do - # Configure as if a replica database is not enabled - allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(true) - end - - it_behaves_like 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch' - it_behaves_like 'does not log any detail' - end - end - - context 'when the total number of records to be inserted is less than the batch size' do - let(:per_batch_size) { 5 } - - it_behaves_like 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch' - it_behaves_like 'does not log any detail' - end - end - - describe '.delete_all_in_batches_for_project' do - let_it_be(:project) { create(:project) } - let_it_be(:user_1) { create(:user) } - let_it_be(:user_2) { create(:user) } - let_it_be(:user_3) { create(:user) } - let_it_be(:user_4) { create(:user) } - - let(:user_ids) { [user_1.id, user_2.id, user_3.id] } - - subject(:execute) do - described_class.delete_all_in_batches_for_project( - project: project, - user_ids: user_ids, - per_batch: per_batch_size - ) - end - - before do - # Configure as if a replica database is enabled - allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false) - end - - before_all do - create(:project_authorization, user: user_1, project: project) - create(:project_authorization, user: user_2, project: project) - create(:project_authorization, user: user_3, project: project) - create(:project_authorization, user: user_4, project: project) - end - - shared_examples_for 'removes the project authorizations of the specified users in the current project, without a delay between each batch' do - specify do - expect(described_class).not_to receive(:sleep) - - execute - - expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids) - end - end - - context 'when the total number of records to be removed is greater than the batch size' do - let(:per_batch_size) { 2 } - - it 'removes the project authorizations of the specified users in the current project, with a delay between each batch' do - expect(described_class).to receive(:sleep).twice - - execute - - expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids) - end - - it_behaves_like 'logs the detail', batch_size: 2 - - context 'when the GitLab installation does not have a replica database configured' do - before do - # Configure as if a replica database is not enabled - allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(true) - end - - it_behaves_like 'removes the project authorizations of the specified users in the current project, without a delay between each batch' - it_behaves_like 'does not log any detail' - end - end - - context 'when the total number of records to be removed is less than the batch size' do - let(:per_batch_size) { 5 } - - it_behaves_like 'removes the project authorizations of the specified users in the current project, without a delay between each batch' - it_behaves_like 'does not log any detail' - end - end - - describe '.delete_all_in_batches_for_user' do - let_it_be(:user) { create(:user) } - let_it_be(:project_1) { create(:project) } - let_it_be(:project_2) { create(:project) } - let_it_be(:project_3) { create(:project) } - let_it_be(:project_4) { create(:project) } - - let(:project_ids) { [project_1.id, project_2.id, project_3.id] } - - subject(:execute) do - described_class.delete_all_in_batches_for_user( - user: user, - project_ids: project_ids, - per_batch: per_batch_size - ) - end - - before do - # Configure as if a replica database is enabled - allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false) - end - - before_all do - create(:project_authorization, user: user, project: project_1) - create(:project_authorization, user: user, project: project_2) - create(:project_authorization, user: user, project: project_3) - create(:project_authorization, user: user, project: project_4) - end - - shared_examples_for 'removes the project authorizations of the specified projects from the current user, without a delay between each batch' do - specify do - expect(described_class).not_to receive(:sleep) - - execute - - expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids) - end - end - - context 'when the total number of records to be removed is greater than the batch size' do - let(:per_batch_size) { 2 } - - it 'removes the project authorizations of the specified projects from the current user, with a delay between each batch' do - expect(described_class).to receive(:sleep).twice - - execute - - expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids) - end - - it_behaves_like 'logs the detail', batch_size: 2 - - context 'when the GitLab installation does not have a replica database configured' do - before do - # Configure as if a replica database is not enabled - allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(true) - end - - it_behaves_like 'removes the project authorizations of the specified projects from the current user, without a delay between each batch' - it_behaves_like 'does not log any detail' - end - end - - context 'when the total number of records to be removed is less than the batch size' do - let(:per_batch_size) { 5 } - - it_behaves_like 'removes the project authorizations of the specified projects from the current user, without a delay between each batch' - it_behaves_like 'does not log any detail' - end - end end diff --git a/spec/models/project_authorizations/changes_spec.rb b/spec/models/project_authorizations/changes_spec.rb new file mode 100644 index 00000000000..1c29179b9d2 --- /dev/null +++ b/spec/models/project_authorizations/changes_spec.rb @@ -0,0 +1,286 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ProjectAuthorizations::Changes, feature_category: :groups_and_projects do + describe '.apply!' do + subject(:apply_project_authorization_changes) { project_authorization_changes.apply! } + + shared_examples_for 'does not log any detail' do + it 'does not log any detail' do + expect(Gitlab::AppLogger).not_to receive(:info) + + apply_project_authorization_changes + end + end + + shared_examples_for 'logs the detail' do |batch_size:| + it 'logs the detail' do + expect(Gitlab::AppLogger).to receive(:info).with( + entire_size: 3, + message: 'Project authorizations refresh performed with delay', + total_delay: (3 / batch_size.to_f).ceil * ProjectAuthorizations::Changes::SLEEP_DELAY, + **Gitlab::ApplicationContext.current + ) + + apply_project_authorization_changes + end + end + + context 'when new authorizations should be added' do + let_it_be(:user) { create(:user) } + let_it_be(:project_1) { create(:project) } + let_it_be(:project_2) { create(:project) } + let_it_be(:project_3) { create(:project) } + + let(:authorizations_to_add) do + [ + { user_id: user.id, project_id: project_1.id, access_level: Gitlab::Access::MAINTAINER }, + { user_id: user.id, project_id: project_2.id, access_level: Gitlab::Access::MAINTAINER }, + { user_id: user.id, project_id: project_3.id, access_level: Gitlab::Access::MAINTAINER } + ] + end + + let(:project_authorization_changes) do + ProjectAuthorizations::Changes.new do |changes| + changes.add(authorizations_to_add) + end + end + + before do + # Configure as if a replica database is enabled + allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false) + end + + shared_examples_for 'inserts the rows in batches, as per the `per_batch` size, without a delay between batches' do + specify do + expect(project_authorization_changes).not_to receive(:sleep) + + apply_project_authorization_changes + + expect(user.project_authorizations.pluck(:user_id, :project_id, + :access_level)).to match_array(authorizations_to_add.map(&:values)) + end + end + + context 'when the total number of records to be inserted is greater than the batch size' do + before do + stub_const("#{described_class}::BATCH_SIZE", 2) + end + + it 'inserts the rows in batches, as per the `per_batch` size, with a delay between each batch' do + expect(ProjectAuthorization).to receive(:insert_all).twice.and_call_original + expect(project_authorization_changes).to receive(:sleep).twice + + apply_project_authorization_changes + + expect(user.project_authorizations.pluck(:user_id, :project_id, + :access_level)).to match_array(authorizations_to_add.map(&:values)) + end + + it_behaves_like 'logs the detail', batch_size: 2 + + context 'when the GitLab installation does not have a replica database configured' do + before do + # Configure as if a replica database is not enabled + allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(true) + end + + it_behaves_like 'inserts the rows in batches, as per the `per_batch` size, without a delay between batches' + it_behaves_like 'does not log any detail' + end + end + + context 'when the total number of records to be inserted is less than the batch size' do + before do + stub_const("#{described_class}::BATCH_SIZE", 5) + end + + it_behaves_like 'inserts the rows in batches, as per the `per_batch` size, without a delay between batches' + it_behaves_like 'does not log any detail' + end + end + + context 'when authorizations should be deleted for a project' do + let_it_be(:project) { create(:project) } + let_it_be(:user_1) { create(:user) } + let_it_be(:user_2) { create(:user) } + let_it_be(:user_3) { create(:user) } + let_it_be(:user_4) { create(:user) } + + let(:user_ids) { [user_1.id, user_2.id, user_3.id] } + + let(:project_authorization_changes) do + ProjectAuthorizations::Changes.new do |changes| + changes.remove_users_in_project(project, user_ids) + end + end + + before do + # Configure as if a replica database is enabled + allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false) + end + + before_all do + create(:project_authorization, user: user_1, project: project) + create(:project_authorization, user: user_2, project: project) + create(:project_authorization, user: user_3, project: project) + create(:project_authorization, user: user_4, project: project) + end + + shared_examples_for 'removes project authorizations of the users in the current project, without a delay' do + specify do + expect(project_authorization_changes).not_to receive(:sleep) + + apply_project_authorization_changes + + expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids) + end + end + + shared_examples_for 'does not removes project authorizations of the users in the current project' do + it 'does not delete any project authorization' do + expect { apply_project_authorization_changes }.not_to change { project.project_authorizations.count } + end + end + + context 'when the total number of records to be removed is greater than the batch size' do + before do + stub_const("#{described_class}::BATCH_SIZE", 2) + end + + it 'removes project authorizations of the users in the current project, with a delay' do + expect(project_authorization_changes).to receive(:sleep).twice + + apply_project_authorization_changes + + expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids) + end + + it_behaves_like 'logs the detail', batch_size: 2 + + context 'when the GitLab installation does not have a replica database configured' do + before do + # Configure as if a replica database is not enabled + allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(true) + end + + it_behaves_like 'removes project authorizations of the users in the current project, without a delay' + it_behaves_like 'does not log any detail' + end + end + + context 'when the total number of records to be removed is less than the batch size' do + before do + stub_const("#{described_class}::BATCH_SIZE", 5) + end + + it_behaves_like 'removes project authorizations of the users in the current project, without a delay' + it_behaves_like 'does not log any detail' + end + + context 'when the user_ids list is empty' do + let(:user_ids) { [] } + + it_behaves_like 'does not removes project authorizations of the users in the current project' + end + + context 'when the user_ids list is nil' do + let(:user_ids) { nil } + + it_behaves_like 'does not removes project authorizations of the users in the current project' + end + end + + describe 'when authorizations should be deleted for an user' do + let_it_be(:user) { create(:user) } + let_it_be(:project_1) { create(:project) } + let_it_be(:project_2) { create(:project) } + let_it_be(:project_3) { create(:project) } + let_it_be(:project_4) { create(:project) } + + let(:project_ids) { [project_1.id, project_2.id, project_3.id] } + + let(:project_authorization_changes) do + ProjectAuthorizations::Changes.new do |changes| + changes.remove_projects_for_user(user, project_ids) + end + end + + before do + # Configure as if a replica database is enabled + allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false) + end + + before_all do + create(:project_authorization, user: user, project: project_1) + create(:project_authorization, user: user, project: project_2) + create(:project_authorization, user: user, project: project_3) + create(:project_authorization, user: user, project: project_4) + end + + shared_examples_for 'removes project authorizations of projects from the current user, without a delay' do + specify do + expect(project_authorization_changes).not_to receive(:sleep) + + apply_project_authorization_changes + + expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids) + end + end + + shared_examples_for 'does not removes any project authorizations from the current user' do + it 'does not delete any project authorization' do + expect { apply_project_authorization_changes }.not_to change { user.project_authorizations.count } + end + end + + context 'when the total number of records to be removed is greater than the batch size' do + before do + stub_const("#{described_class}::BATCH_SIZE", 2) + end + + it 'removes the project authorizations of projects from the current user, with a delay between each batch' do + expect(project_authorization_changes).to receive(:sleep).twice + + apply_project_authorization_changes + + expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids) + end + + it_behaves_like 'logs the detail', batch_size: 2 + + context 'when the GitLab installation does not have a replica database configured' do + before do + # Configure as if a replica database is not enabled + allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(true) + end + + it_behaves_like 'removes project authorizations of projects from the current user, without a delay' + it_behaves_like 'does not log any detail' + end + end + + context 'when the total number of records to be removed is less than the batch size' do + before do + stub_const("#{described_class}::BATCH_SIZE", 5) + end + + it_behaves_like 'removes project authorizations of projects from the current user, without a delay' + it_behaves_like 'does not log any detail' + end + + context 'when the project_ids list is empty' do + let(:project_ids) { [] } + + it_behaves_like 'does not removes any project authorizations from the current user' + end + + context 'when the user_ids list is nil' do + let(:project_ids) { nil } + + it_behaves_like 'does not removes any project authorizations from the current user' + end + end + end +end diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index 71c205fca7c..090173bc999 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -259,13 +259,30 @@ RSpec.describe ProjectStatistics do end describe '#update_repository_size' do - before do - allow(project.repository).to receive(:size).and_return(12) - statistics.update_repository_size + context 'with recent_objects_for_project_statistics enabled' do + before do + stub_feature_flags(recent_objects_for_project_statistics: true) + allow(project.repository).to receive(:recent_objects_size).and_return(5) + + statistics.update_repository_size + end + + it 'stores the size of the repository' do + expect(statistics.repository_size).to eq 5.megabytes + end end - it "stores the size of the repository" do - expect(statistics.repository_size).to eq 12.megabytes + context 'with use_recent_objects_for_project_statistics disabled' do + before do + stub_feature_flags(recent_objects_for_project_statistics: false) + allow(project.repository).to receive(:size).and_return(10) + + statistics.update_repository_size + end + + it 'stores the size of the repository' do + expect(statistics.repository_size).to eq 10.megabytes + end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index aa2ac52a9ab..9f9417c2483 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2400,6 +2400,7 @@ RSpec.describe Repository, feature_category: :source_code_management do expect(repository).to receive(:expire_method_caches).with( [ :size, + :recent_objects_size, :commit_count, :readme_path, :contribution_guide, @@ -2874,7 +2875,7 @@ RSpec.describe Repository, feature_category: :source_code_management do describe '#expire_statistics_caches' do it 'expires the caches' do expect(repository).to receive(:expire_method_caches) - .with(%i(size commit_count)) + .with(%i(size recent_objects_size commit_count)) repository.expire_statistics_caches end @@ -3006,6 +3007,22 @@ RSpec.describe Repository, feature_category: :source_code_management do end end + describe '#recent_objects_size' do + context 'with a non-existing repository' do + it 'returns 0' do + expect(repository).to receive(:exists?).and_return(false) + + expect(repository.recent_objects_size).to eq(0.0) + end + end + + context 'with an existing repository' do + it 'returns the repository recent_objects_size as a Float' do + expect(repository.recent_objects_size).to be_an_instance_of(Float) + end + end + end + describe '#local_branches' do it 'returns the local branches' do masterrev = repository.find_branch('master').dereferenced_target diff --git a/spec/policies/ci/bridge_policy_spec.rb b/spec/policies/ci/bridge_policy_spec.rb index e598e2f7626..d23355b4c1e 100644 --- a/spec/policies/ci/bridge_policy_spec.rb +++ b/spec/policies/ci/bridge_policy_spec.rb @@ -13,6 +13,8 @@ RSpec.describe Ci::BridgePolicy do described_class.new(user, bridge) end + it_behaves_like 'a deployable job policy', :ci_bridge + describe '#play_job' do before do fake_access = double('Gitlab::UserAccess') diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 041b70b10d4..6ab89daff82 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::BuildPolicy do +RSpec.describe Ci::BuildPolicy, feature_category: :continuous_integration do let(:user) { create(:user) } let(:build) { create(:ci_build, pipeline: pipeline) } let(:pipeline) { create(:ci_empty_pipeline, project: project) } @@ -11,6 +11,8 @@ RSpec.describe Ci::BuildPolicy do described_class.new(user, build) end + it_behaves_like 'a deployable job policy', :ci_build + shared_context 'public pipelines disabled' do before do project.update_attribute(:public_builds, false) diff --git a/spec/requests/api/admin/plan_limits_spec.rb b/spec/requests/api/admin/plan_limits_spec.rb index 97eb8a2b13f..f0bc90efa50 100644 --- a/spec/requests/api/admin/plan_limits_spec.rb +++ b/spec/requests/api/admin/plan_limits_spec.rb @@ -102,7 +102,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d 'ci_registered_group_runners': 107, 'ci_registered_project_runners': 108, 'conan_max_file_size': 10, - 'enforcement_limit': 15, + 'enforcement_limit': 100, 'generic_packages_max_file_size': 20, 'helm_max_file_size': 25, 'maven_max_file_size': 30, @@ -124,11 +124,11 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared d expect(json_response['ci_registered_group_runners']).to eq(107) expect(json_response['ci_registered_project_runners']).to eq(108) expect(json_response['conan_max_file_size']).to eq(10) - expect(json_response['enforcement_limit']).to eq(15) + expect(json_response['enforcement_limit']).to eq(100) expect(json_response['generic_packages_max_file_size']).to eq(20) expect(json_response['helm_max_file_size']).to eq(25) expect(json_response['limits_history']).to eq( - { "enforcement_limit" => [{ "user_id" => admin.id, "username" => admin.username, "timestamp" => current_timestamp, "value" => 15 }], + { "enforcement_limit" => [{ "user_id" => admin.id, "username" => admin.username, "timestamp" => current_timestamp, "value" => 100 }], "notification_limit" => [{ "user_id" => admin.id, "username" => admin.username, "timestamp" => current_timestamp, "value" => 90 }], "storage_size_limit" => [{ "user_id" => admin.id, "username" => admin.username, "timestamp" => current_timestamp, "value" => 80 }] } ) diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 33fac72e025..01cf789d3b9 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -59,6 +59,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['group_runner_token_expiration_interval']).to be_nil expect(json_response['project_runner_token_expiration_interval']).to be_nil expect(json_response['max_export_size']).to eq(0) + expect(json_response['max_decompressed_archive_size']).to eq(25600) expect(json_response['max_terraform_state_size_bytes']).to eq(0) expect(json_response['pipeline_limit_per_project_user_sha']).to eq(0) expect(json_response['delete_inactive_projects']).to be(false) @@ -169,6 +170,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu mailgun_events_enabled: true, mailgun_signing_key: 'MAILGUN_SIGNING_KEY', max_export_size: 6, + max_decompressed_archive_size: 20000, max_terraform_state_size_bytes: 1_000, disabled_oauth_sign_in_sources: 'unknown', import_sources: 'github,bitbucket', @@ -248,6 +250,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['mailgun_events_enabled']).to be(true) expect(json_response['mailgun_signing_key']).to eq('MAILGUN_SIGNING_KEY') expect(json_response['max_export_size']).to eq(6) + expect(json_response['max_decompressed_archive_size']).to eq(20000) expect(json_response['max_terraform_state_size_bytes']).to eq(1_000) expect(json_response['disabled_oauth_sign_in_sources']).to eq([]) expect(json_response['import_sources']).to match_array(%w(github bitbucket)) diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb index 5b7da9ce84f..4131f1d26ec 100644 --- a/spec/requests/ide_controller_spec.rb +++ b/spec/requests/ide_controller_spec.rb @@ -187,7 +187,7 @@ RSpec.describe IdeController, feature_category: :web_ide do it 'updates the content security policy with the correct frame sources' do subject - expect(find_csp_directive('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/") + expect(find_csp_directive('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.web-ide.gitlab-static.net/") expect(find_csp_directive('worker-src')).to include("http://www.example.com/assets/webpack/") end diff --git a/spec/requests/web_ide/remote_ide_controller_spec.rb b/spec/requests/web_ide/remote_ide_controller_spec.rb index 62f5cb90e0a..d4248af6482 100644 --- a/spec/requests/web_ide/remote_ide_controller_spec.rb +++ b/spec/requests/web_ide/remote_ide_controller_spec.rb @@ -73,7 +73,7 @@ RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_developmen end it "updates the content security policy with the correct frame sources" do - expect(find_csp_directive('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/") + expect(find_csp_directive('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.web-ide.gitlab-static.net/") end end diff --git a/spec/services/admin/plan_limits/update_service_spec.rb b/spec/services/admin/plan_limits/update_service_spec.rb index 718367fadc2..e57c234780c 100644 --- a/spec/services/admin/plan_limits/update_service_spec.rb +++ b/spec/services/admin/plan_limits/update_service_spec.rb @@ -16,10 +16,10 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do ci_registered_group_runners: 107, ci_registered_project_runners: 108, conan_max_file_size: 10, - enforcement_limit: 15, + enforcement_limit: 100, generic_packages_max_file_size: 20, helm_max_file_size: 25, - notification_limit: 30, + notification_limit: 95, maven_max_file_size: 40, npm_max_file_size: 60, nuget_max_file_size: 60, @@ -50,10 +50,10 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do expect(limits.limits_history).to eq( { "enforcement_limit" => [{ "user_id" => user.id, "username" => user.username, - "timestamp" => current_timestamp, "value" => 15 }], + "timestamp" => current_timestamp, "value" => 100 }], "notification_limit" => [{ "user_id" => user.id, "username" => user.username, - "timestamp" => current_timestamp, "value" => 30 }], + "timestamp" => current_timestamp, "value" => 95 }], "storage_size_limit" => [{ "user_id" => user.id, "username" => user.username, "timestamp" => current_timestamp, "value" => 90 }] } @@ -68,13 +68,122 @@ RSpec.describe Admin::PlanLimits::UpdateService, feature_category: :shared do end context 'when the update is unsuccessful' do - let(:params) { { notification_limit: 'abc' } } + context 'when notification_limit is less than storage_size_limit' do + let(:params) { { notification_limit: 2 } } + + before do + limits.update!( + storage_size_limit: 5, + enforcement_limit: 10 + ) + end - it 'returns an error' do - response = update_plan_limits + it 'returns an error' do + response = update_plan_limits + + expect(response[:status]).to eq :error + expect(response[:message]).to eq ["Notification limit must be greater than or equal to " \ + "storage_size_limit (Dashboard limit): 5 " \ + "and less than or equal to enforcement_limit: 10"] + end + end + + context 'when notification_limit is greater than enforcement_limit' do + let(:params) { { notification_limit: 11 } } + + before do + limits.update!( + storage_size_limit: 5, + enforcement_limit: 10 + ) + end + + it 'returns an error' do + response = update_plan_limits + + expect(response[:status]).to eq :error + expect(response[:message]).to eq ["Notification limit must be greater than or equal to " \ + "storage_size_limit (Dashboard limit): 5 " \ + "and less than or equal to enforcement_limit: 10"] + end + end + + context 'when enforcement_limit is less than storage_size_limit' do + let(:params) { { enforcement_limit: 9 } } + + before do + limits.update!( + storage_size_limit: 12, + notification_limit: 12 + ) + end + + it 'returns an error' do + response = update_plan_limits + + expect(response[:status]).to eq :error + expect(response[:message]).to eq ["Enforcement limit must be greater than " \ + "or equal to storage_size_limit (Dashboard limit): " \ + "12 and greater than or equal to notification_limit: 12"] + end + end - expect(response[:status]).to eq :error - expect(response[:message]).to include 'Notification limit is not a number' + context 'when enforcement_limit is less than notification_limit' do + let(:params) { { enforcement_limit: 9 } } + + before do + limits.update!( + storage_size_limit: 10, + notification_limit: 10 + ) + end + + it 'returns an error' do + response = update_plan_limits + + expect(response[:status]).to eq :error + expect(response[:message]).to eq ["Enforcement limit must be greater than or equal to " \ + "storage_size_limit (Dashboard limit): " \ + "10 and greater than or equal to notification_limit: 10"] + end + end + + context 'when storage_size_limit is greater than notification_limit' do + let(:params) { { storage_size_limit: 11 } } + + before do + limits.update!( + enforcement_limit: 12, + notification_limit: 10 + ) + end + + it 'returns an error' do + response = update_plan_limits + + expect(response[:status]).to eq :error + expect(response[:message]).to eq ["Storage size limit (Dashboard limit) must be less than or " \ + "equal to enforcement_limit: 12 and notification_limit: 10"] + end + end + + context 'when storage_size_limit is greater than enforcement_limit' do + let(:params) { { storage_size_limit: 11 } } + + before do + limits.update!( + enforcement_limit: 10, + notification_limit: 11 + ) + end + + it 'returns an error' do + response = update_plan_limits + + expect(response[:status]).to eq :error + expect(response[:message]).to eq ["Storage size limit (Dashboard limit) must be less than or " \ + "equal to enforcement_limit: 10 and notification_limit: 11"] + end end end end diff --git a/spec/services/metrics/dashboard/default_embed_service_spec.rb b/spec/services/metrics/dashboard/default_embed_service_spec.rb deleted file mode 100644 index 6ef248f6b09..00000000000 --- a/spec/services/metrics/dashboard/default_embed_service_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Metrics::Dashboard::DefaultEmbedService, :use_clean_rails_memory_store_caching, - feature_category: :metrics do - include MetricsDashboardHelpers - - let_it_be(:project) { build(:project) } - let_it_be(:user) { create(:user) } - let_it_be(:environment) { create(:environment, project: project) } - - before do - project.add_maintainer(user) if user - end - - describe '.valid_params?' do - let(:params) { { embedded: true } } - - subject { described_class.valid_params?(params) } - - it { is_expected.to be_truthy } - - context 'missing embedded' do - let(:params) { {} } - - it { is_expected.to be_falsey } - end - - context 'not embedded' do - let(:params) { { embedded: 'false' } } - - it { is_expected.to be_falsey } - end - end - - describe '#get_dashboard' do - let(:service_params) { [project, user, { environment: environment }] } - let(:service_call) { described_class.new(*service_params).get_dashboard } - - it_behaves_like 'valid embedded dashboard service response' - it_behaves_like 'raises error for users with insufficient permissions' - - it 'caches the unprocessed dashboard for subsequent calls' do - system_service = Metrics::Dashboard::SystemDashboardService - - expect(system_service).to receive(:new).once.and_call_original - - described_class.new(*service_params).get_dashboard - described_class.new(*service_params).get_dashboard - end - - context 'when called with a non-system dashboard' do - let(:dashboard_path) { 'garbage/dashboard/path' } - - it_behaves_like 'valid embedded dashboard service response' - end - end -end diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb index 762378c93ec..f6565853460 100644 --- a/spec/services/projects/update_statistics_service_spec.rb +++ b/spec/services/projects/update_statistics_service_spec.rb @@ -23,13 +23,13 @@ RSpec.describe Projects::UpdateStatisticsService, feature_category: :groups_and_ let_it_be(:project) { create(:project) } where(:statistics, :method_caches) do - [] | %i(size commit_count) - ['repository_size'] | [:size] - [:repository_size] | [:size] + [] | %i(size recent_objects_size commit_count) + ['repository_size'] | %i(size recent_objects_size) + [:repository_size] | %i(size recent_objects_size) [:lfs_objects_size] | nil [:commit_count] | [:commit_count] - [:repository_size, :commit_count] | %i(size commit_count) - [:repository_size, :commit_count, :lfs_objects_size] | %i(size commit_count) + [:repository_size, :commit_count] | %i(size recent_objects_size commit_count) + [:repository_size, :commit_count, :lfs_objects_size] | %i(size recent_objects_size commit_count) end with_them do diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index 55b27954a74..6bd0162f0f7 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -108,10 +108,7 @@ RSpec.describe Users::RefreshAuthorizedProjectsService, feature_category: :user_ describe '#update_authorizations' do context 'when there are no rows to add and remove' do it 'does not change authorizations' do - expect(ProjectAuthorization).not_to receive(:delete_all_in_batches_for_user) - expect(ProjectAuthorization).not_to receive(:insert_all_in_batches) - - service.update_authorizations([], []) + expect { service.update_authorizations([], []) }.to not_change { user.project_authorizations.count } end end diff --git a/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb b/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb new file mode 100644 index 00000000000..cb7001a9faf --- /dev/null +++ b/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handle subscription based on user access' do + it 'subscribes to the noteable stream when user has access' do + subscribe(subscribe_params) + + expect(subscription).to be_confirmed + expect(subscription).to have_stream_for(noteable) + end + + it 'rejects the subscription when the user does not have access' do + stub_action_cable_connection current_user: nil + + subscribe(subscribe_params) + + expect(subscription).to be_rejected + end + + context 'when action_cable_notes is disabled' do + before do + stub_feature_flags(action_cable_notes: false) + end + + it 'rejects the subscription' do + subscribe(subscribe_params) + + expect(subscription).to be_rejected + end + end +end diff --git a/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb b/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb new file mode 100644 index 00000000000..73bdc094237 --- /dev/null +++ b/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a deployable job policy' do |factory_type| + let_it_be_with_refind(:project) { create(:project, :private) } + let_it_be_with_refind(:user) { create(:user) } + + let(:job) { create(factory_type, project: project, user: user, environment: 'production', ref: 'development') } + let(:policy) { described_class.new(user, job) } + + context 'when the job triggerer is a project maintainer' do + before_all do + project.add_maintainer(user) + end + + it { expect(policy).to be_allowed :update_build } + + context 'when job is oudated deployment job' do + before do + allow(job).to receive(:outdated_deployment?).and_return(true) + end + + it { expect(policy).not_to be_allowed :update_build } + end + end +end diff --git a/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb b/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb new file mode 100644 index 00000000000..b1057b3f67a --- /dev/null +++ b/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a deployable job policy in EE' do |factory_type| + using RSpec::Parameterized::TableSyntax + + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, group: group) } + + let(:user) { create(:user) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + let(:environment) { create(:environment, project: project, name: 'production') } + + let(:job) do + create(factory_type, pipeline: pipeline, project: project, environment: 'production', ref: 'development') + end + + describe '#update_build?' do + subject { user.can?(:update_build, job) } + + it_behaves_like 'protected environments access', direct_access: true + end + + describe '#update_commit_status?' do + subject { user.can?(:update_commit_status, job) } + + it_behaves_like 'protected environments access', direct_access: true + end + + describe '#erase_build?' do + subject { user.can?(:erase_build, job) } + + context 'when the job triggerer is a project maintainer' do + let_it_be_with_refind(:user) { create(:user).tap { |u| project.add_maintainer(u) } } + + before do + stub_licensed_features(protected_environments: true) + end + + it 'returns true for ci_build' do + # Currently, we allow users to delete normal jobs only. + if factory_type == :ci_build + is_expected.to eq(true) + else + is_expected.to eq(false) + end + end + + context 'when environment is protected' do + before do + create(:protected_environment, name: environment.name, project: project) + end + + it { is_expected.to eq(false) } + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb index a33a846417b..33b62564e5f 100644 --- a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true RSpec.shared_examples 'a hook that gets automatically disabled on failure' do + let(:logger) { instance_double('Gitlab::WebHooks::Logger') } + + before do + allow(hook).to receive(:logger).and_return(logger) + allow(logger).to receive(:info) + end + shared_examples 'is tolerant of invalid records' do specify do hook.url = nil @@ -171,6 +178,23 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do expect { hook.enable! }.to change { hook.executable? }.from(false).to(true) end + it 'logs relevant information' do + hook.recent_failures = 1000 + hook.disabled_until = 1.hour.from_now + + expect(logger) + .to receive(:info) + .with(a_hash_including( + hook_id: hook.id, + action: 'enable', + recent_failures: 0, + disabled_until: nil, + backoff_count: 0 + )) + + hook.enable! + end + it 'does not update hooks unless necessary' do hook @@ -188,11 +212,25 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do end end - describe '#backoff!' do + describe '#backoff!', :freeze_time do context 'when we have not backed off before' do it 'does not disable the hook' do expect { hook.backoff! }.not_to change { hook.executable? }.from(true) end + + it 'increments recent_failures' do + expect { hook.backoff! }.to change { hook.recent_failures }.from(0).to(1) + end + + it 'logs relevant information' do + expect(logger) + .to receive(:info) + .with(a_hash_including( + hook_id: hook.id, action: 'backoff', recent_failures: 1 + )) + + hook.backoff! + end end context 'when we have exhausted the grace period' do @@ -200,6 +238,32 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD) end + it 'disables the hook' do + expect { hook.backoff! }.to change { hook.executable? }.from(true).to(false) + end + + it 'increments backoff_count' do + expect { hook.backoff! }.to change { hook.backoff_count }.from(0).to(1) + end + + it 'sets disabled_until' do + expect { hook.backoff! }.to change { hook.disabled_until }.from(nil).to(1.minute.from_now) + end + + it 'logs relevant information' do + expect(logger) + .to receive(:info) + .with(a_hash_including( + hook_id: hook.id, + action: 'backoff', + recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1, + disabled_until: 1.minute.from_now, + backoff_count: 1 + )) + + hook.backoff! + end + context 'when the hook is permanently disabled' do before do allow(hook).to receive(:permanently_disabled?).and_return(true) @@ -218,15 +282,15 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do def run_expectation expect { hook.backoff! }.to change { hook.backoff_count }.by(1) end + end - context 'when the flag is disabled' do - before do - stub_feature_flags(auto_disabling_web_hooks: false) - end + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end - it 'does not increment backoff count' do - expect { hook.failed! }.not_to change { hook.backoff_count } - end + it 'does not increment backoff count' do + expect { hook.failed! }.not_to change { hook.backoff_count } end end end @@ -250,36 +314,6 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do end end - describe '#disable!' do - it 'disables a hook' do - expect { hook.disable! }.to change { hook.executable? }.from(true).to(false) - end - - context 'when the flag is disabled' do - before do - stub_feature_flags(auto_disabling_web_hooks: false) - end - - it 'does not disable the hook' do - expect { hook.disable! }.not_to change { hook.executable? } - end - end - - it 'does nothing if the hook is already disabled' do - allow(hook).to receive(:permanently_disabled?).and_return(true) - - sql_count = ActiveRecord::QueryRecorder.new { hook.disable! }.count - - expect(sql_count).to eq(0) - end - - include_examples 'is tolerant of invalid records' do - def run_expectation - expect { hook.disable! }.to change { hook.executable? }.from(true).to(false) - end - end - end - describe '#temporarily_disabled?' do it 'is false when not temporarily disabled' do expect(hook).not_to be_temporarily_disabled @@ -324,7 +358,7 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do context 'when hook has been disabled' do before do - hook.disable! + hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD) end it 'is true' do @@ -350,7 +384,7 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do context 'when hook has been disabled' do before do - hook.disable! + hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD) end it { is_expected.to eq :disabled } @@ -366,7 +400,7 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do context 'when hook has been backed off' do before do - hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1) + hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD) hook.disabled_until = 1.hour.from_now end diff --git a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb index 8cadad0959b..32e36c74a73 100644 --- a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb @@ -137,12 +137,6 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur end end - describe '#disable!' do - it 'does not disable a group hook' do - expect { hook.disable! }.not_to change { hook.executable? }.from(true) - end - end - describe '#temporarily_disabled?' do it 'is false' do # Initially @@ -164,7 +158,7 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur # Initially expect(hook).not_to be_permanently_disabled - hook.disable! + hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD) expect(hook).not_to be_permanently_disabled end @@ -177,7 +171,7 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur context 'when hook has been disabled' do before do - hook.disable! + hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD) end it { is_expected.to eq :executable } @@ -185,7 +179,7 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur context 'when hook has been backed off' do before do - hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1) + hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD) hook.disabled_until = 1.hour.from_now end diff --git a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb index a2c34aa6a54..7489dc7c1d6 100644 --- a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb @@ -121,7 +121,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| context 'the hook is disabled' do before do - hook.disable! + hook.update!(recent_failures: hook.class::EXCEEDED_FAILURE_THRESHOLD) end it "has the correct alert status", :aggregate_failures do diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb index 5ae0a8695eb..0305aa3012c 100644 --- a/spec/tooling/danger/project_helper_spec.rb +++ b/spec/tooling/danger/project_helper_spec.rb @@ -75,8 +75,6 @@ RSpec.describe Tooling::Danger::ProjectHelper do 'ee/spec/frontend/bar' | [:frontend] 'ee/spec/frontend_integration/bar' | [:frontend] - '.gitlab/ci/frontend.gitlab-ci.yml' | %i[frontend tooling] - 'app/models/foo' | [:backend] 'bin/foo' | [:backend] 'config/foo' | [:backend] @@ -116,35 +114,6 @@ RSpec.describe Tooling::Danger::ProjectHelper do 'Rakefile' | [:backend] 'FOO_VERSION' | [:backend] - 'scripts/glfm/bar.rb' | [:backend] - 'scripts/glfm/bar.js' | [:frontend] - 'scripts/remote_development/run-smoke-test-suite.sh' | [:remote_development_be] - 'scripts/lib/glfm/bar.rb' | [:backend] - 'scripts/lib/glfm/bar.js' | [:frontend] - 'scripts/bar.rb' | [:backend, :tooling] - 'scripts/bar.js' | [:frontend, :tooling] - 'scripts/subdir/bar.rb' | [:backend, :tooling] - 'scripts/subdir/bar.js' | [:frontend, :tooling] - 'scripts/foo' | [:tooling] - - 'Dangerfile' | [:tooling] - 'danger/bundle_size/Dangerfile' | [:tooling] - 'ee/danger/bundle_size/Dangerfile' | [:tooling] - 'danger/bundle_size/' | [:tooling] - 'ee/danger/bundle_size/' | [:tooling] - '.gitlab-ci.yml' | [:tooling] - '.gitlab/ci/cng.gitlab-ci.yml' | [:tooling] - '.gitlab/ci/ee-specific-checks.gitlab-ci.yml' | [:tooling] - 'tooling/danger/foo' | [:tooling] - 'ee/tooling/danger/foo' | [:tooling] - 'lefthook.yml' | [:tooling] - '.editorconfig' | [:tooling] - 'tooling/bin/find_foss_tests' | [:tooling] - '.codeclimate.yml' | [:tooling] - '.gitlab/CODEOWNERS' | [:tooling] - 'gems/gem.gitlab-ci.yml' | [:tooling] - 'gems/config/rubocop.yml' | [:tooling] - 'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | [:ci_template] 'lib/gitlab/ci/templates/dotNET-Core.yml' | [:ci_template] diff --git a/spec/workers/web_hook_worker_spec.rb b/spec/workers/web_hook_worker_spec.rb index be43b83ec0a..cd58dd93b80 100644 --- a/spec/workers/web_hook_worker_spec.rb +++ b/spec/workers/web_hook_worker_spec.rb @@ -7,10 +7,14 @@ RSpec.describe WebHookWorker, feature_category: :integrations do let_it_be(:project_hook) { create(:project_hook) } let_it_be(:data) { { foo: 'bar' } } let_it_be(:hook_name) { 'push_hooks' } + let_it_be(:response) { ServiceResponse.success } describe '#perform' do it 'delegates to WebHookService' do - expect_next(WebHookService, project_hook, data.with_indifferent_access, hook_name, anything).to receive(:execute) + expect_next(WebHookService, project_hook, data.with_indifferent_access, hook_name, anything) + .to receive(:execute).and_return(response) + expect(subject).to receive(:log_extra_metadata_on_done).with(:response_status, response.status) + expect(subject).to receive(:log_extra_metadata_on_done).with(:http_status, response[:http_status]) subject.perform(project_hook.id, data, hook_name) end @@ -23,7 +27,11 @@ RSpec.describe WebHookWorker, feature_category: :integrations do uuid = SecureRandom.uuid params = { recursion_detection_request_uuid: uuid } - expect_next(WebHookService, project_hook, data.with_indifferent_access, hook_name, anything).to receive(:execute) + expect_next(WebHookService, project_hook, data.with_indifferent_access, hook_name, anything) + .to receive(:execute).and_return(response) + expect(subject).to receive(:log_extra_metadata_on_done).with(:response_status, response.status) + expect(subject).to receive(:log_extra_metadata_on_done).with(:http_status, response[:http_status]) + expect { subject.perform(project_hook.id, data, hook_name, params) } .to change { Gitlab::WebHooks::RecursionDetection::UUID.instance.request_uuid }.to(uuid) end diff --git a/tooling/danger/project_helper.rb b/tooling/danger/project_helper.rb index f7c8f4f5133..84dbf7568c3 100644 --- a/tooling/danger/project_helper.rb +++ b/tooling/danger/project_helper.rb @@ -106,28 +106,6 @@ module Tooling %r{\A((ee|jh)/)?app/finders/} => [:database, :backend], %r{\Arubocop/cop/migration(/|\.rb)} => :database, - %r{\A(\.ruby-version\z|\.nvmrc\z|\.tool-versions\z)} => :tooling, - %r{\A(\.gitlab-ci\.yml\z|\.gitlab/ci)} => :tooling, - %r{\A\.codeclimate\.yml\z} => :tooling, - %r{\Alefthook.yml\z} => :tooling, - %r{\A\.editorconfig\z} => :tooling, - %r{Dangerfile\z} => :tooling, - %r{\A((ee|jh)/)?(danger/|tooling/danger/)} => :tooling, - %r{\Agems/gem\.gitlab-ci\.yml\z} => :tooling, - %r{\Agems/config/} => :tooling, - - %r{\A((ee|jh)/)?scripts/(lib/)?glfm/.*\.rb} => [:backend], - %r{\A((ee|jh)/)?scripts/(lib/)?glfm/.*\.js} => [:frontend], - %r{\A((ee|jh)/)?scripts/remote_development/.*} => [:remote_development_be], - %r{\A((ee|jh)/)?scripts/.*\.rb} => [:backend, :tooling], - %r{\A((ee|jh)/)?scripts/.*\.js} => [:frontend, :tooling], - %r{\A((ee|jh)/)?scripts/} => :tooling, - - %r{\Atooling/} => :tooling, - %r{(CODEOWNERS)} => :tooling, - %r{(tests.yml)} => :tooling, - %r{\A\.gitpod\.yml} => :tooling, - %r{\Alib/gitlab/ci/templates} => :ci_template, %r{\A((ee|jh)/)?spec/features/} => :test, diff --git a/yarn.lock b/yarn.lock index c354545a914..fc60e294131 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1151,10 +1151,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235" integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g== -"@gitlab/web-ide@0.0.1-dev-20230713160749-patch-1": - version "0.0.1-dev-20230713160749-patch-1" - resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230713160749-patch-1.tgz#6420b55aae444533f9a4bd6269503d98a72aaa2e" - integrity sha512-Dh8XQyPwDY6fkd/A+hTHCqrD23u5qnlaxKu5myyxDEgBNGgu4SGblFU9B6NHNm8eGUZk6Cs5MuMk+NUvWRKbmA== +"@gitlab/web-ide@0.0.1-dev-20230802205337": + version "0.0.1-dev-20230802205337" + resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230802205337.tgz#bd1954486e9d615d65864cfa9ce4876ebc2a29a4" + integrity sha512-cek6IixB+oW39iYwyce+x9yiHWdZn4EwDeXCLgdzWpYl74tccKFfkjt2ZTtWsfZUKGE+xJkoeTE+ZKnoeaUSPA== "@graphql-eslint/eslint-plugin@3.20.1": version "3.20.1" |