Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-12-10 18:10:24 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-12-10 18:10:24 +0300
commitecc11e5d608ff4393fb6c44d02416569e7d2785d (patch)
treed6e2921cf11f525d8fd7bbbab213684983dba0cf
parente838c62efb5d95fe76b5bbb6cba8b73c40eb2008 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/rails/include_url_helper.yml4
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/issues/form.js7
-rw-r--r--app/assets/javascripts/issues/new/components/title_suggestions.vue (renamed from app/assets/javascripts/issues/suggestions/components/app.vue)8
-rw-r--r--app/assets/javascripts/issues/new/components/title_suggestions_item.vue (renamed from app/assets/javascripts/issues/suggestions/components/item.vue)0
-rw-r--r--app/assets/javascripts/issues/new/components/type_popover.vue (renamed from app/assets/javascripts/issues/type_selector/components/info_popover.vue)4
-rw-r--r--app/assets/javascripts/issues/new/index.js (renamed from app/assets/javascripts/issues/suggestions/index.js)32
-rw-r--r--app/assets/javascripts/issues/new/queries/issues.query.graphql (renamed from app/assets/javascripts/issues/suggestions/queries/issues.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/type_selector/index.js16
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js11
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue14
-rw-r--r--app/assets/javascripts/runner/constants.js1
-rw-r--r--app/assets/javascripts/runner/graphql/runner_node.fragment.graphql1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/controllers/application_controller.rb1
-rw-r--r--app/controllers/concerns/check_rate_limit.rb22
-rw-r--r--app/controllers/concerns/integrations/hooks_execution.rb12
-rw-r--r--app/controllers/concerns/notes_actions.rb7
-rw-r--r--app/controllers/groups_controller.rb10
-rw-r--r--app/controllers/import/base_controller.rb16
-rw-r--r--app/controllers/import/gitlab_groups_controller.rb11
-rw-r--r--app/controllers/profiles/emails_controller.rb16
-rw-r--r--app/controllers/projects/hooks_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb18
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb10
-rw-r--r--app/controllers/projects/raw_controller.rb18
-rw-r--r--app/controllers/projects/repositories_controller.rb16
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb14
-rw-r--r--app/models/ci/namespace_mirror.rb30
-rw-r--r--app/models/ci/project_mirror.rb9
-rw-r--r--app/models/container_repository.rb11
-rw-r--r--app/models/error_tracking/error_event.rb3
-rw-r--r--app/models/loose_foreign_keys/deleted_record.rb36
-rw-r--r--app/models/namespace.rb12
-rw-r--r--app/models/namespaces/sync_event.rb16
-rw-r--r--app/models/project.rb12
-rw-r--r--app/models/projects/sync_event.rb16
-rw-r--r--app/policies/namespaces/user_namespace_policy.rb3
-rw-r--r--app/presenters/ci/pipeline_presenter.rb9
-rw-r--r--app/presenters/prometheus_alert_presenter.rb2
-rw-r--r--app/services/auth/container_registry_authentication_service.rb2
-rw-r--r--app/services/ci/process_sync_events_service.rb58
-rw-r--r--app/services/ci/retry_build_service.rb10
-rw-r--r--app/services/search_service.rb2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml4
-rw-r--r--app/workers/all_queues.yml18
-rw-r--r--app/workers/namespaces/process_sync_events_worker.rb22
-rw-r--r--app/workers/projects/process_sync_events_worker.rb22
-rw-r--r--config/feature_flags/development/api_v3_commits_skip_diff_files.yml8
-rw-r--r--config/feature_flags/development/ci_namespace_project_mirrors.yml (renamed from config/feature_flags/development/clone_job_variables_at_job_retry.yml)8
-rw-r--r--config/feature_flags/development/lfk_automatic_partition_creation.yml8
-rw-r--r--config/feature_flags/development/lfk_automatic_partition_dropping.yml8
-rw-r--r--config/initializers/postgres_partitioning.rb3
-rw-r--r--config/sidekiq_queues.yml4
-rw-r--r--db/fixtures/development/32_crm.rb47
-rw-r--r--db/migrate/20211011140932_create_namespaces_sync_events.rb9
-rw-r--r--db/migrate/20211011141239_create_projects_sync_events.rb9
-rw-r--r--db/migrate/20211011141242_create_namespaces_sync_trigger.rb37
-rw-r--r--db/migrate/20211011141243_create_projects_sync_trigger.rb37
-rw-r--r--db/migrate/20211202094944_move_loose_fk_deleted_records_to_dynamic_schema.rb17
-rw-r--r--db/schema_migrations/202110111409321
-rw-r--r--db/schema_migrations/202110111412391
-rw-r--r--db/schema_migrations/202110111412421
-rw-r--r--db/schema_migrations/202110111412431
-rw-r--r--db/schema_migrations/202112020949441
-rw-r--r--db/structure.sql151
-rw-r--r--doc/administration/gitaly/recovery.md2
-rw-r--r--doc/administration/pages/index.md4
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/api/topics.md2
-rw-r--r--doc/architecture/blueprints/cloud_native_gitlab_pages/index.md2
-rw-r--r--doc/architecture/blueprints/container_registry_metadata_database/index.md4
-rw-r--r--doc/ci/ci_cd_for_external_repos/index.md2
-rw-r--r--doc/ci/pipelines/merge_trains.md2
-rw-r--r--doc/ci/pipelines/pipelines_for_merged_results.md2
-rw-r--r--doc/ci/test_cases/index.md2
-rw-r--r--doc/development/go_guide/go_upgrade.md6
-rw-r--r--doc/development/go_guide/index.md10
-rw-r--r--doc/development/integrations/secure.md2
-rw-r--r--doc/development/secure_coding_guidelines.md2
-rw-r--r--doc/development/shell_scripting_guide/index.md2
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/push_rules/push_rules.md2
-rw-r--r--doc/subscriptions/index.md2
-rw-r--r--doc/update/upgrading_from_source.md2
-rw-r--r--doc/user/application_security/api_fuzzing/index.md2
-rw-r--r--doc/user/application_security/container_scanning/index.md2
-rw-r--r--doc/user/application_security/dast/browser_based.md18
-rw-r--r--doc/user/application_security/dast/index.md2
-rw-r--r--doc/user/application_security/dependency_scanning/index.md4
-rw-r--r--doc/user/application_security/index.md2
-rw-r--r--doc/user/application_security/security_dashboard/index.md2
-rw-r--r--doc/user/clusters/agent/index.md2
-rw-r--r--doc/user/compliance/license_compliance/index.md16
-rw-r--r--doc/user/group/epics/epic_boards.md2
-rw-r--r--doc/user/group/epics/index.md2
-rw-r--r--doc/user/group/saml_sso/index.md2
-rw-r--r--doc/user/packages/go_proxy/index.md4
-rw-r--r--doc/user/project/clusters/serverless/index.md2
-rw-r--r--doc/user/project/code_owners.md2
-rw-r--r--doc/user/project/pages/lets_encrypt_for_gitlab_pages.md2
-rw-r--r--doc/user/project/requirements/index.md2
-rw-r--r--doc/user/project/working_with_projects.md2
-rw-r--r--doc/user/search/advanced_search.md2
-rw-r--r--lib/api/group_export.rb4
-rw-r--r--lib/api/helpers/rate_limiter.rb27
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/project_export.rb4
-rw-r--r--lib/api/project_import.rb4
-rw-r--r--lib/api/repositories.rb6
-rw-r--r--lib/api/v3/github.rb2
-rw-r--r--lib/gitlab/application_rate_limiter.rb9
-rw-r--r--lib/gitlab/ci/badge/metadata.rb3
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml2
-rw-r--r--lib/gitlab/database/schema_helpers.rb1
-rw-r--r--lib/gitlab/database/type/json_pg_safe.rb24
-rw-r--r--lib/gitlab/rate_limit_helpers.rb35
-rw-r--r--lib/gitlab/repository_archive_rate_limiter.rb13
-rw-r--r--locale/gitlab.pot15
-rw-r--r--spec/controllers/profiles/emails_controller_spec.rb2
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb11
-rw-r--r--spec/features/admin/admin_runners_spec.rb13
-rw-r--r--spec/fixtures/error_tracking/parsed_event_nullbytes.json175
-rw-r--r--spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap (renamed from spec/frontend/issues/type_selector/components/__snapshots__/info_popover_spec.js.snap)6
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_item_spec.js (renamed from spec/frontend/issues/suggestions/components/item_spec.js)6
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_spec.js (renamed from spec/frontend/issues/suggestions/components/app_spec.js)10
-rw-r--r--spec/frontend/issues/new/components/type_popover_spec.js (renamed from spec/frontend/issues/type_selector/components/info_popover_spec.js)6
-rw-r--r--spec/frontend/issues/new/mock_data.js (renamed from spec/frontend/issues/suggestions/mock_data.js)0
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js11
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js38
-rw-r--r--spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb9
-rw-r--r--spec/lib/gitlab/database/type/json_pg_safe_spec.rb26
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/rate_limit_helpers_spec.rb50
-rw-r--r--spec/lib/gitlab/repository_archive_rate_limiter_spec.rb56
-rw-r--r--spec/models/ci/namespace_mirror_spec.rb94
-rw-r--r--spec/models/ci/project_mirror_spec.rb36
-rw-r--r--spec/models/container_repository_spec.rb33
-rw-r--r--spec/models/loose_foreign_keys/deleted_record_spec.rb149
-rw-r--r--spec/models/namespace_spec.rb71
-rw-r--r--spec/models/project_spec.rb71
-rw-r--r--spec/models/user_spec.rb6
-rw-r--r--spec/policies/namespaces/user_namespace_policy_spec.rb22
-rw-r--r--spec/requests/api/error_tracking/collector_spec.rb12
-rw-r--r--spec/requests/api/v3/github_spec.rb12
-rw-r--r--spec/services/ci/process_sync_events_service_spec.rb129
-rw-r--r--spec/services/ci/retry_build_service_spec.rb24
-rw-r--r--spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb2
-rw-r--r--spec/services/search_service_spec.rb21
-rw-r--r--spec/workers/namespaces/process_sync_events_worker_spec.rb32
-rw-r--r--spec/workers/projects/process_sync_events_worker_spec.rb28
154 files changed, 1861 insertions, 511 deletions
diff --git a/.rubocop_todo/rails/include_url_helper.yml b/.rubocop_todo/rails/include_url_helper.yml
index adc42663523..2dedba19c78 100644
--- a/.rubocop_todo/rails/include_url_helper.yml
+++ b/.rubocop_todo/rails/include_url_helper.yml
@@ -21,20 +21,16 @@ Rails/IncludeUrlHelper:
- app/models/integrations/webex_teams.rb
- app/models/integrations/youtrack.rb
- app/presenters/alert_management/alert_presenter.rb
- - app/presenters/ci/pipeline_presenter.rb
- app/presenters/environment_presenter.rb
- app/presenters/gitlab/blame_presenter.rb
- app/presenters/merge_request_presenter.rb
- app/presenters/project_presenter.rb
- - app/presenters/prometheus_alert_presenter.rb
- app/presenters/release_presenter.rb
- app/presenters/releases/evidence_presenter.rb
- ee/app/helpers/license_helper.rb
- ee/app/models/integrations/github.rb
- - ee/app/presenters/merge_request_approver_presenter.rb
- ee/spec/helpers/ee/projects/security/configuration_helper_spec.rb
- ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb
- - lib/gitlab/ci/badge/metadata.rb
- spec/helpers/merge_requests_helper_spec.rb
- spec/helpers/nav/top_nav_helper_spec.rb
- spec/helpers/notify_helper_spec.rb
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 21ff8d35786..b9abc36b3c3 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-4ba8618078d9107d52c0d735f76286ab0b113a8a
+1de88e4247d4b940f843003781cb2bf75582b826
diff --git a/app/assets/javascripts/issues/form.js b/app/assets/javascripts/issues/form.js
index 20a8c251304..33371d065f9 100644
--- a/app/assets/javascripts/issues/form.js
+++ b/app/assets/javascripts/issues/form.js
@@ -4,8 +4,7 @@ import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GLForm from '~/gl_form';
-import initSuggestions from '~/issues/suggestions';
-import initIssuableTypeSelector from '~/issues/type_selector';
+import { initTitleSuggestions, initTypePopover } from '~/issues/new';
import LabelsSelect from '~/labels/labels_select';
import MilestoneSelect from '~/milestones/milestone_select';
import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
@@ -20,6 +19,6 @@ export default () => {
warnTemplateOverride: true,
});
- initSuggestions();
- initIssuableTypeSelector();
+ initTitleSuggestions();
+ initTypePopover();
};
diff --git a/app/assets/javascripts/issues/suggestions/components/app.vue b/app/assets/javascripts/issues/new/components/title_suggestions.vue
index 48a5e220abf..0a9cdb12519 100644
--- a/app/assets/javascripts/issues/suggestions/components/app.vue
+++ b/app/assets/javascripts/issues/new/components/title_suggestions.vue
@@ -2,12 +2,12 @@
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import query from '../queries/issues.query.graphql';
-import Suggestion from './item.vue';
+import TitleSuggestionsItem from './title_suggestions_item.vue';
export default {
components: {
- Suggestion,
GlIcon,
+ TitleSuggestionsItem,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -66,7 +66,7 @@ export default {
</script>
<template>
- <div v-show="showSuggestions" class="form-group row issuable-suggestions">
+ <div v-show="showSuggestions" class="form-group row">
<div v-once class="col-form-label col-sm-2 pt-0">
{{ __('Similar issues') }}
<gl-icon
@@ -86,7 +86,7 @@ export default {
'gl-mb-3': index !== issues.length - 1,
}"
>
- <suggestion :suggestion="suggestion" />
+ <title-suggestions-item :suggestion="suggestion" />
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/issues/suggestions/components/item.vue b/app/assets/javascripts/issues/new/components/title_suggestions_item.vue
index a01f4f747b9..a01f4f747b9 100644
--- a/app/assets/javascripts/issues/suggestions/components/item.vue
+++ b/app/assets/javascripts/issues/new/components/title_suggestions_item.vue
diff --git a/app/assets/javascripts/issues/type_selector/components/info_popover.vue b/app/assets/javascripts/issues/new/components/type_popover.vue
index 3a20ccba814..a70e79b70f9 100644
--- a/app/assets/javascripts/issues/type_selector/components/info_popover.vue
+++ b/app/assets/javascripts/issues/new/components/type_popover.vue
@@ -19,9 +19,9 @@ export default {
<template>
<span id="popovercontainer">
- <gl-icon id="issuable-type-info" name="question-o" class="gl-ml-5 gl-text-gray-500" />
+ <gl-icon id="issue-type-info" name="question-o" class="gl-ml-5 gl-text-gray-500" />
<gl-popover
- target="issuable-type-info"
+ target="issue-type-info"
container="popovercontainer"
:title="$options.i18n.issueTypes"
triggers="focus hover"
diff --git a/app/assets/javascripts/issues/suggestions/index.js b/app/assets/javascripts/issues/new/index.js
index 8f7f317d6b4..59a7cbec627 100644
--- a/app/assets/javascripts/issues/suggestions/index.js
+++ b/app/assets/javascripts/issues/new/index.js
@@ -1,14 +1,19 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import App from './components/app.vue';
+import TitleSuggestions from './components/title_suggestions.vue';
+import TypePopover from './components/type_popover.vue';
-Vue.use(VueApollo);
+export function initTitleSuggestions() {
+ Vue.use(VueApollo);
-export default function initIssuableSuggestions() {
const el = document.getElementById('js-suggestions');
const issueTitle = document.getElementById('issue_title');
- const { projectPath } = el.dataset;
+
+ if (!el) {
+ return undefined;
+ }
+
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
@@ -26,13 +31,26 @@ export default function initIssuableSuggestions() {
this.search = issueTitle.value;
});
},
- render(h) {
- return h(App, {
+ render(createElement) {
+ return createElement(TitleSuggestions, {
props: {
- projectPath,
+ projectPath: el.dataset.projectPath,
search: this.search,
},
});
},
});
}
+
+export function initTypePopover() {
+ const el = document.getElementById('js-type-popover');
+
+ if (!el) {
+ return undefined;
+ }
+
+ return new Vue({
+ el,
+ render: (createElement) => createElement(TypePopover),
+ });
+}
diff --git a/app/assets/javascripts/issues/suggestions/queries/issues.query.graphql b/app/assets/javascripts/issues/new/queries/issues.query.graphql
index dc0757b141f..dc0757b141f 100644
--- a/app/assets/javascripts/issues/suggestions/queries/issues.query.graphql
+++ b/app/assets/javascripts/issues/new/queries/issues.query.graphql
diff --git a/app/assets/javascripts/issues/type_selector/index.js b/app/assets/javascripts/issues/type_selector/index.js
deleted file mode 100644
index 433a62d1ae8..00000000000
--- a/app/assets/javascripts/issues/type_selector/index.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Vue from 'vue';
-import InfoPopover from './components/info_popover.vue';
-
-export default function initIssuableTypeSelector() {
- const el = document.getElementById('js-type-popover');
-
- return new Vue({
- el,
- components: {
- InfoPopover,
- },
- render(h) {
- return h(InfoPopover);
- },
- });
-}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index a82dad7e2c9..7235b38848c 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -735,3 +735,14 @@ export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag];
export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i));
export const isLoggedIn = () => Boolean(window.gon?.current_user_id);
+
+/**
+ * This method takes in array of objects with snake_case
+ * property names and returns a new array of objects with
+ * camelCase property names
+ *
+ * @param {Array[Object]} array - Array to be converted
+ * @returns {Array[Object]} Converted array
+ */
+export const convertArrayOfObjectsToCamelCase = (array) =>
+ array.map((o) => convertObjectPropsToCamelCase(o));
diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue
index f96eb0fa564..023308dbac2 100644
--- a/app/assets/javascripts/runner/components/runner_list.vue
+++ b/app/assets/javascripts/runner/components/runner_list.vue
@@ -2,8 +2,9 @@
import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { __, s__ } from '~/locale';
+import { formatNumber, __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { RUNNER_JOB_COUNT_LIMIT } from '../constants';
import RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerSummaryCell from './cells/runner_summary_cell.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue';
@@ -52,6 +53,12 @@ export default {
},
},
methods: {
+ formatJobCount(jobCount) {
+ if (jobCount > RUNNER_JOB_COUNT_LIMIT) {
+ return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`;
+ }
+ return formatNumber(jobCount);
+ },
runnerTrAttr(runner) {
if (runner) {
return {
@@ -66,6 +73,7 @@ export default {
tableField({ key: 'summary', label: s__('Runners|Runner ID'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'version', label: __('Version') }),
tableField({ key: 'ipAddress', label: __('IP Address') }),
+ tableField({ key: 'jobCount', label: __('Jobs') }),
tableField({ key: 'tagList', label: __('Tags'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'contactedAt', label: __('Last contact') }),
tableField({ key: 'actions', label: '' }),
@@ -112,6 +120,10 @@ export default {
</tooltip-on-truncate>
</template>
+ <template #cell(jobCount)="{ item: { jobCount } }">
+ {{ formatJobCount(jobCount) }}
+ </template>
+
<template #cell(tagList)="{ item: { tagList } }">
<runner-tags :tag-list="tagList" size="sm" />
</template>
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index f0aa15ef64c..68e45fcf8e9 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -1,6 +1,7 @@
import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
+export const RUNNER_JOB_COUNT_LIMIT = 1000;
export const GROUP_RUNNER_COUNT_LIMIT = 1000;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
diff --git a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
index 3828d725758..169f6ffd2ea 100644
--- a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
@@ -8,6 +8,7 @@ fragment RunnerNode on CiRunner {
ipAddress
active
locked
+ jobCount
tagList
contactedAt
status(legacyMode: null)
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 327135d17ab..8600a4059d8 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -869,10 +869,6 @@
}
}
-.issuable-suggestions svg {
- vertical-align: sub;
-}
-
.suggestion-footer {
font-size: 12px;
line-height: 15px;
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 976a9fa5038..d3ecbdcc1f6 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -23,6 +23,7 @@ class ApplicationController < ActionController::Base
include Gitlab::Utils::StrongMemoize
include ::Gitlab::EndpointAttributes
include FlocOptOut
+ include CheckRateLimit
before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms?
diff --git a/app/controllers/concerns/check_rate_limit.rb b/app/controllers/concerns/check_rate_limit.rb
index c4de3315e22..5ccdf843525 100644
--- a/app/controllers/concerns/check_rate_limit.rb
+++ b/app/controllers/concerns/check_rate_limit.rb
@@ -5,19 +5,27 @@
# Controller concern that checks if the rate limit for a given action is throttled by calling the
# Gitlab::ApplicationRateLimiter class. If the action is throttled for the current user, the request
# will be logged and an error message will be rendered with a Too Many Requests response status.
+# See lib/api/helpers/rate_limiter.rb for API version
module CheckRateLimit
- def check_rate_limit(key)
- return unless rate_limiter.throttled?(key, scope: current_user, users_allowlist: rate_limit_users_allowlist)
+ def check_rate_limit!(key, scope:, redirect_back: false, **options)
+ return unless rate_limiter.throttled?(key, scope: scope, **options)
rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
- render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
+
+ return yield if block_given?
+
+ message = _('This endpoint has been requested too many times. Try again later.')
+
+ if redirect_back
+ redirect_back_or_default(options: { alert: message })
+ else
+ render plain: message, status: :too_many_requests
+ end
end
+ private
+
def rate_limiter
::Gitlab::ApplicationRateLimiter
end
-
- def rate_limit_users_allowlist
- Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist
- end
end
diff --git a/app/controllers/concerns/integrations/hooks_execution.rb b/app/controllers/concerns/integrations/hooks_execution.rb
index af039057a9c..6a9d3d51f9b 100644
--- a/app/controllers/concerns/integrations/hooks_execution.rb
+++ b/app/controllers/concerns/integrations/hooks_execution.rb
@@ -32,16 +32,4 @@ module Integrations::HooksExecution
flash[:alert] = "Hook execution failed: #{message}"
end
end
-
- def create_rate_limit(key, scope)
- if rate_limiter.throttled?(key, scope: [scope, current_user])
- rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
-
- render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
- end
- end
-
- def rate_limiter
- ::Gitlab::ApplicationRateLimiter
- end
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 113030429d8..8410a8779f6 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -3,7 +3,6 @@
module NotesActions
include RendersNotes
include Gitlab::Utils::StrongMemoize
- include CheckRateLimit
extend ActiveSupport::Concern
# last_fetched_at is an integer number of microseconds, which is the same
@@ -16,7 +15,11 @@ module NotesActions
before_action :require_noteable!, only: [:index, :create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :note_project, only: [:create]
- before_action -> { check_rate_limit(:notes_create) }, only: [:create]
+ before_action -> {
+ check_rate_limit!(:notes_create,
+ scope: current_user,
+ users_allowlist: Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist)
+ }, only: [:create]
end
def index
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 7f2b026b5b3..62336c7eede 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -37,7 +37,7 @@ class GroupsController < Groups::ApplicationController
push_frontend_feature_flag(:iteration_cadences, @group, default_enabled: :yaml)
end
- before_action :export_rate_limit, only: [:export, :download_export]
+ before_action :check_export_rate_limit!, only: [:export, :download_export]
helper_method :captcha_required?
@@ -314,16 +314,12 @@ class GroupsController < Groups::ApplicationController
url_for(safe_params)
end
- def export_rate_limit
+ def check_export_rate_limit!
prefixed_action = "group_#{params[:action]}".to_sym
scope = params[:action] == :download_export ? @group : nil
- if Gitlab::ApplicationRateLimiter.throttled?(prefixed_action, scope: [current_user, scope].compact)
- Gitlab::ApplicationRateLimiter.log_request(request, "#{prefixed_action}_request_limit".to_sym, current_user)
-
- render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
- end
+ check_rate_limit!(prefixed_action, scope: [current_user, scope].compact)
end
def ensure_export_enabled
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 53856e4575b..7ad3a2ee358 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -3,7 +3,7 @@
class Import::BaseController < ApplicationController
include ActionView::Helpers::SanitizeHelper
- before_action :import_rate_limit, only: [:create]
+ before_action -> { check_rate_limit!(:project_import, scope: [current_user, :project_import], redirect_back: true) }, only: [:create]
feature_category :importers
def status
@@ -98,18 +98,4 @@ class Import::BaseController < ApplicationController
def project_save_error(project)
project.errors.full_messages.join(', ')
end
-
- def import_rate_limit
- key = "project_import".to_sym
-
- if rate_limiter.throttled?(key, scope: [current_user, key])
- rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
-
- redirect_back_or_default(options: { alert: _('This endpoint has been requested too many times. Try again later.') })
- end
- end
-
- def rate_limiter
- ::Gitlab::ApplicationRateLimiter
- end
end
diff --git a/app/controllers/import/gitlab_groups_controller.rb b/app/controllers/import/gitlab_groups_controller.rb
index 503b10f766b..aca71f6d57a 100644
--- a/app/controllers/import/gitlab_groups_controller.rb
+++ b/app/controllers/import/gitlab_groups_controller.rb
@@ -4,7 +4,7 @@ class Import::GitlabGroupsController < ApplicationController
include WorkhorseAuthorization
before_action :ensure_group_import_enabled
- before_action :import_rate_limit, only: %i[create]
+ before_action :check_import_rate_limit!, only: %i[create]
feature_category :importers
@@ -55,12 +55,9 @@ class Import::GitlabGroupsController < ApplicationController
render_404 unless Feature.enabled?(:group_import_export, @group, default_enabled: true)
end
- def import_rate_limit
- if Gitlab::ApplicationRateLimiter.throttled?(:group_import, scope: current_user)
- Gitlab::ApplicationRateLimiter.log_request(request, :group_import_request_limit, current_user)
-
- flash[:alert] = _('This endpoint has been requested too many times. Try again later.')
- redirect_to new_group_path
+ def check_import_rate_limit!
+ check_rate_limit!(:group_import, scope: current_user) do
+ redirect_to new_group_path, alert: _('This endpoint has been requested too many times. Try again later.')
end
end
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index 6e5b18cb885..be2cb270a19 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -2,8 +2,10 @@
class Profiles::EmailsController < Profiles::ApplicationController
before_action :find_email, only: [:destroy, :resend_confirmation_instructions]
- before_action -> { rate_limit!(:profile_add_new_email) }, only: [:create]
- before_action -> { rate_limit!(:profile_resend_email_confirmation) }, only: [:resend_confirmation_instructions]
+ before_action -> { check_rate_limit!(:profile_add_new_email, scope: current_user, redirect_back: true) },
+ only: [:create]
+ before_action -> { check_rate_limit!(:profile_resend_email_confirmation, scope: current_user, redirect_back: true) },
+ only: [:resend_confirmation_instructions]
feature_category :users
@@ -42,16 +44,6 @@ class Profiles::EmailsController < Profiles::ApplicationController
private
- def rate_limit!(action)
- rate_limiter = ::Gitlab::ApplicationRateLimiter
-
- if rate_limiter.throttled?(action, scope: current_user)
- rate_limiter.log_request(request, action, current_user)
-
- redirect_back_or_default(options: { alert: _('This action has been performed too many times. Try again later.') })
- end
- end
-
def email_params
params.require(:email).permit(:email)
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index c79e5a8cc85..99eba32e00f 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -6,7 +6,7 @@ class Projects::HooksController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :hook_logs, only: :edit
- before_action -> { create_rate_limit(:project_testing_hook, @project) }, only: :test
+ before_action -> { check_rate_limit!(:project_testing_hook, scope: [@project, current_user]) }, only: :test
respond_to :html
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index d2d7ecfab6f..970efd9cdfb 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -37,7 +37,9 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_download_code!, only: [:related_branches]
# Limit the amount of issues created per minute
- before_action :create_rate_limit, only: [:create], if: -> { Feature.disabled?('rate_limited_service_issues_create', project, default_enabled: :yaml) }
+ before_action -> { check_rate_limit!(:issues_create, scope: [@project, @current_user])},
+ only: [:create],
+ if: -> { Feature.disabled?('rate_limited_service_issues_create', project, default_enabled: :yaml) }
before_action do
push_frontend_feature_flag(:tribute_autocomplete, @project)
@@ -363,20 +365,6 @@ class Projects::IssuesController < Projects::ApplicationController
project_compare_path(project, from: project.default_branch, to: branch[:name])
end
- def create_rate_limit
- key = :issues_create
-
- if rate_limiter.throttled?(key, scope: [@project, @current_user])
- rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
-
- render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
- end
- end
-
- def rate_limiter
- ::Gitlab::ApplicationRateLimiter
- end
-
def service_desk?
action_name == 'service_desk'
end
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index 4af7508b935..ac94cc001dd 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -3,7 +3,7 @@
class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :schedule, except: [:index, :new, :create]
- before_action :play_rate_limit, only: [:play]
+ before_action :check_play_rate_limit!, only: [:play]
before_action :authorize_play_pipeline_schedule!, only: [:play]
before_action :authorize_read_pipeline_schedule!
before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
@@ -81,19 +81,15 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
private
- def play_rate_limit
+ def check_play_rate_limit!
return unless current_user
- if rate_limiter.throttled?(:play_pipeline_schedule, scope: [current_user, schedule])
+ check_rate_limit!(:play_pipeline_schedule, scope: [current_user, schedule]) do
flash[:alert] = _('You cannot play this scheduled pipeline at the moment. Please wait a minute.')
redirect_to pipeline_schedules_path(@project)
end
end
- def rate_limiter
- ::Gitlab::ApplicationRateLimiter
- end
-
def schedule
@schedule ||= project.pipeline_schedules.find(params[:id])
end
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 8960783400e..9707b70f26f 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -13,7 +13,7 @@ class Projects::RawController < Projects::ApplicationController
before_action :set_ref_and_path
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :show_rate_limit, only: [:show], unless: :external_storage_request?
+ before_action :check_show_rate_limit!, only: [:show], unless: :external_storage_request?
before_action :redirect_to_external_storage, only: :show, if: :static_objects_external_storage_enabled?
feature_category :source_code_management
@@ -33,23 +33,11 @@ class Projects::RawController < Projects::ApplicationController
@ref, @path = extract_ref(get_id)
end
- def show_rate_limit
- if rate_limiter.throttled?(:show_raw_controller, scope: [@project, @path], threshold: raw_blob_request_limit)
- rate_limiter.log_request(request, :raw_blob_request_limit, current_user)
-
+ def check_show_rate_limit!
+ check_rate_limit!(:raw_blob, scope: [@project, @path]) do
render plain: _('You cannot access the raw file. Please wait a minute.'), status: :too_many_requests
end
end
-
- def rate_limiter
- ::Gitlab::ApplicationRateLimiter
- end
-
- def raw_blob_request_limit
- Gitlab::CurrentSettings
- .current_application_settings
- .raw_blob_request_limit
- end
end
Projects::RawController.prepend_mod
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index 8beebb52980..77826a2f789 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -3,16 +3,16 @@
class Projects::RepositoriesController < Projects::ApplicationController
include ExtractsPath
include StaticObjectExternalStorage
- include Gitlab::RateLimitHelpers
include HotlinkInterceptor
+ include Gitlab::RepositoryArchiveRateLimiter
prepend_before_action(only: [:archive]) { authenticate_sessionless_user!(:archive) }
skip_before_action :default_cache_headers, only: :archive
# Authorize
+ before_action :check_archive_rate_limiting!, only: :archive
before_action :require_non_empty_project, except: :create
- before_action :archive_rate_limit!, only: :archive
before_action :intercept_hotlinking!, only: :archive
before_action :assign_archive_vars, only: :archive
before_action :assign_append_sha, only: :archive
@@ -42,12 +42,6 @@ class Projects::RepositoriesController < Projects::ApplicationController
private
- def archive_rate_limit!
- if archive_rate_limit_reached?(current_user, @project)
- render plain: ::Gitlab::RateLimitHelpers::ARCHIVE_RATE_LIMIT_REACHED_MESSAGE, status: :too_many_requests
- end
- end
-
def repo_params
@repo_params ||= { ref: @ref, path: params[:path], format: params[:format], append_sha: @append_sha }
end
@@ -125,6 +119,12 @@ class Projects::RepositoriesController < Projects::ApplicationController
[path, nil]
end
end
+
+ def check_archive_rate_limiting!
+ check_archive_rate_limit!(current_user, @project) do
+ render(plain: _('This archive has been requested too many times. Try again later.'), status: :too_many_requests)
+ end
+ end
end
Projects::RepositoriesController.prepend_mod_with('Projects::RepositoriesController')
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 887f98362b4..ef6c10d43cd 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -9,6 +9,7 @@ module Projects
layout 'project_settings'
before_action :authorize_admin_pipeline!
+ before_action :check_builds_available!
before_action :define_variables
before_action do
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 6af46e22d8f..428903a9e75 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -30,7 +30,7 @@ class ProjectsController < Projects::ApplicationController
before_action :event_filter, only: [:show, :activity]
# Project Export Rate Limit
- before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export]
+ before_action :check_export_rate_limit!, only: [:export, :download_export, :generate_new_export]
before_action do
push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml)
@@ -544,20 +544,12 @@ class ProjectsController < Projects::ApplicationController
@project = @project.present(current_user: current_user)
end
- def export_rate_limit
+ def check_export_rate_limit!
prefixed_action = "project_#{params[:action]}".to_sym
project_scope = params[:action] == 'download_export' ? @project : nil
- if rate_limiter.throttled?(prefixed_action, scope: [current_user, project_scope].compact)
- rate_limiter.log_request(request, "#{prefixed_action}_request_limit".to_sym, current_user)
-
- render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
- end
- end
-
- def rate_limiter
- ::Gitlab::ApplicationRateLimiter
+ check_rate_limit!(prefixed_action, scope: [current_user, project_scope].compact)
end
def render_edit
diff --git a/app/models/ci/namespace_mirror.rb b/app/models/ci/namespace_mirror.rb
index a497d2cabe5..8a4be3139e8 100644
--- a/app/models/ci/namespace_mirror.rb
+++ b/app/models/ci/namespace_mirror.rb
@@ -4,6 +4,34 @@ module Ci
# This model represents a record in a shadow table of the main database's namespaces table.
# It allows us to navigate the namespace hierarchy on the ci database without resorting to a JOIN.
class NamespaceMirror < ApplicationRecord
- # Will be filled by https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75517
+ belongs_to :namespace
+
+ scope :contains_namespace, -> (id) do
+ where('traversal_ids @> ARRAY[?]::int[]', id)
+ end
+
+ class << self
+ def sync!(event)
+ namespace = event.namespace
+ traversal_ids = namespace.self_and_ancestor_ids(hierarchy_order: :desc)
+
+ upsert({ namespace_id: event.namespace_id, traversal_ids: traversal_ids },
+ unique_by: :namespace_id)
+
+ # It won't be necessary once we remove `sync_traversal_ids`.
+ # More info: https://gitlab.com/gitlab-org/gitlab/-/issues/347541
+ sync_children_namespaces!(event.namespace_id, traversal_ids)
+ end
+
+ private
+
+ def sync_children_namespaces!(namespace_id, traversal_ids)
+ contains_namespace(namespace_id)
+ .where.not(namespace_id: namespace_id)
+ .update_all(
+ "traversal_ids = ARRAY[#{sanitize_sql(traversal_ids.join(','))}]::int[] || traversal_ids[array_position(traversal_ids, #{sanitize_sql(namespace_id)}) + 1:]"
+ )
+ end
+ end
end
end
diff --git a/app/models/ci/project_mirror.rb b/app/models/ci/project_mirror.rb
index c6e3101fb3a..d6aaa3f50c1 100644
--- a/app/models/ci/project_mirror.rb
+++ b/app/models/ci/project_mirror.rb
@@ -4,6 +4,13 @@ module Ci
# This model represents a shadow table of the main database's projects table.
# It allows us to navigate the project and namespace hierarchy on the ci database.
class ProjectMirror < ApplicationRecord
- # Will be filled by https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75517
+ belongs_to :project
+
+ class << self
+ def sync!(event)
+ upsert({ project_id: event.project_id, namespace_id: event.project.namespace_id },
+ unique_by: :project_id)
+ end
+ end
end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 8e130998f11..c914819f79d 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -145,9 +145,14 @@ class ContainerRepository < ApplicationRecord
name: path.repository_name)
end
- def self.create_from_path!(path)
- safe_find_or_create_by!(project: path.repository_project,
- name: path.repository_name)
+ def self.find_or_create_from_path(path)
+ repository = safe_find_or_create_by(
+ project: path.repository_project,
+ name: path.repository_name
+ )
+ return repository if repository.persisted?
+
+ find_by_path!(path)
end
def self.build_root_repository(project)
diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb
index 0b638f65768..18c1467e6f6 100644
--- a/app/models/error_tracking/error_event.rb
+++ b/app/models/error_tracking/error_event.rb
@@ -3,6 +3,9 @@
class ErrorTracking::ErrorEvent < ApplicationRecord
belongs_to :error, counter_cache: :events_count
+ # Scrub null bytes
+ attribute :payload, Gitlab::Database::Type::JsonPgSafe.new
+
validates :payload, json_schema: { filename: 'error_tracking_event_payload' }
validates :error, presence: true
diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb
index c3b3e76f67b..0fbdd2d8a5b 100644
--- a/app/models/loose_foreign_keys/deleted_record.rb
+++ b/app/models/loose_foreign_keys/deleted_record.rb
@@ -1,15 +1,45 @@
# frozen_string_literal: true
class LooseForeignKeys::DeletedRecord < ApplicationRecord
+ PARTITION_DURATION = 1.day
+
+ include PartitionedTable
+
self.primary_key = :id
+ self.ignored_columns = %i[partition]
+
+ partitioned_by :partition, strategy: :sliding_list,
+ next_partition_if: -> (active_partition) do
+ return false if Feature.disabled?(:lfk_automatic_partition_creation, default_enabled: :yaml)
+
+ oldest_record_in_partition = LooseForeignKeys::DeletedRecord
+ .select(:id, :created_at)
+ .for_partition(active_partition)
+ .order(:id)
+ .limit(1)
+ .take
+
+ oldest_record_in_partition.present? && oldest_record_in_partition.created_at < PARTITION_DURATION.ago
+ end,
+ detach_partition_if: -> (partition) do
+ return false if Feature.disabled?(:lfk_automatic_partition_dropping, default_enabled: :yaml)
+
+ !LooseForeignKeys::DeletedRecord
+ .for_partition(partition)
+ .status_pending
+ .exists?
+ end
scope :for_table, -> (table) { where(fully_qualified_table_name: table) }
+ scope :for_partition, -> (partition) { where(partition: partition) }
scope :consume_order, -> { order(:partition, :consume_after, :id) }
enum status: { pending: 1, processed: 2 }, _prefix: :status
def self.load_batch_for_table(table, batch_size)
- for_table(table)
+ # selecting partition as partition_number to workaround the sliding partitioning column ignore
+ select(arel_table[Arel.star], arel_table[:partition].as('partition_number'))
+ .for_table(table)
.status_pending
.consume_order
.limit(batch_size)
@@ -20,9 +50,9 @@ class LooseForeignKeys::DeletedRecord < ApplicationRecord
# Run a query for each partition to optimize the row lookup by primary key (partition, id)
update_count = 0
- all_records.group_by(&:partition).each do |partition, records_within_partition|
+ all_records.group_by(&:partition_number).each do |partition, records_within_partition|
update_count += status_pending
- .where(partition: partition)
+ .for_partition(partition)
.where(id: records_within_partition.pluck(:id))
.update_all(status: :processed)
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index db306221318..4b1cf2fa217 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -64,6 +64,9 @@ class Namespace < ApplicationRecord
has_one :admin_note, inverse_of: :namespace
accepts_nested_attributes_for :admin_note, update_only: true
+ has_one :ci_namespace_mirror, class_name: 'Ci::NamespaceMirror'
+ has_many :sync_events, class_name: 'Namespaces::SyncEvent'
+
validates :owner, presence: true, if: ->(n) { n.owner_required? }
validates :name,
presence: true,
@@ -104,6 +107,8 @@ class Namespace < ApplicationRecord
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :avatar_url, to: :owner, allow_nil: true
+ after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_parent_id? }
+
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
before_create :sync_share_with_group_lock_with_parent
@@ -609,6 +614,13 @@ class Namespace < ApplicationRecord
def enforce_minimum_path_length?
path_changed? && !project_namespace?
end
+
+ # SyncEvents are created by PG triggers (with the function `insert_namespaces_sync_event`)
+ def schedule_sync_event_worker
+ run_after_commit do
+ Namespaces::SyncEvent.enqueue_worker
+ end
+ end
end
Namespace.prepend_mod_with('Namespace')
diff --git a/app/models/namespaces/sync_event.rb b/app/models/namespaces/sync_event.rb
new file mode 100644
index 00000000000..8534d8afb8c
--- /dev/null
+++ b/app/models/namespaces/sync_event.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# This model serves to keep track of changes to the namespaces table in the main database, and allowing to safely
+# replicate these changes to other databases.
+class Namespaces::SyncEvent < ApplicationRecord
+ self.table_name = 'namespaces_sync_events'
+
+ belongs_to :namespace
+
+ scope :preload_synced_relation, -> { preload(:namespace) }
+ scope :order_by_id_asc, -> { order(id: :asc) }
+
+ def self.enqueue_worker
+ ::Namespaces::ProcessSyncEventsWorker.perform_async # rubocop:disable CodeReuse/Worker
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 088a2f9ea27..a751e8adeb0 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -102,6 +102,8 @@ class Project < ApplicationRecord
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
+ after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? }
+
after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
after_save :save_topics
@@ -394,6 +396,9 @@ class Project < ApplicationRecord
has_many :timelogs
+ has_one :ci_project_mirror, class_name: 'Ci::ProjectMirror'
+ has_many :sync_events, class_name: 'Projects::SyncEvent'
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :project_setting, update_only: true
@@ -2938,6 +2943,13 @@ class Project < ApplicationRecord
project_namespace.shared_runners_enabled = shared_runners_enabled
project_namespace.visibility_level = visibility_level
end
+
+ # SyncEvents are created by PG triggers (with the function `insert_projects_sync_event`)
+ def schedule_sync_event_worker
+ run_after_commit do
+ Projects::SyncEvent.enqueue_worker
+ end
+ end
end
Project.prepend_mod_with('Project')
diff --git a/app/models/projects/sync_event.rb b/app/models/projects/sync_event.rb
new file mode 100644
index 00000000000..5221b00c55f
--- /dev/null
+++ b/app/models/projects/sync_event.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# This model serves to keep track of changes to the namespaces table in the main database as they relate to projects,
+# allowing to safely replicate changes to other databases.
+class Projects::SyncEvent < ApplicationRecord
+ self.table_name = 'projects_sync_events'
+
+ belongs_to :project
+
+ scope :preload_synced_relation, -> { preload(:project) }
+ scope :order_by_id_asc, -> { order(id: :asc) }
+
+ def self.enqueue_worker
+ ::Projects::ProcessSyncEventsWorker.perform_async # rubocop:disable CodeReuse/Worker
+ end
+end
diff --git a/app/policies/namespaces/user_namespace_policy.rb b/app/policies/namespaces/user_namespace_policy.rb
index 93626c6d1a1..09b0f5d608d 100644
--- a/app/policies/namespaces/user_namespace_policy.rb
+++ b/app/policies/namespaces/user_namespace_policy.rb
@@ -4,7 +4,6 @@ module Namespaces
class UserNamespacePolicy < ::NamespacePolicy
rule { anonymous }.prevent_all
- condition(:personal_project, scope: :subject) { @subject.kind == 'user' }
condition(:can_create_personal_project, scope: :user) { @user.can_create_project? }
condition(:owner) { @subject.owner == @user }
@@ -19,7 +18,7 @@ module Namespaces
enable :read_package_settings
end
- rule { personal_project & ~can_create_personal_project }.prevent :create_projects
+ rule { ~can_create_personal_project }.prevent :create_projects
rule { (owner | admin) & can?(:create_projects) }.enable :transfer_projects
end
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 00820e00863..7f5dffadcfb 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -3,7 +3,6 @@
module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
- include ActionView::Helpers::UrlHelper
delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
delegator_override_with ActionView::Helpers::TagHelper # TODO: Remove `ActionView::Helpers::UrlHelper` inclusion as it overrides `Ci::Pipeline#tag`
@@ -108,7 +107,7 @@ module Ci
end
def link_to_pipeline_ref
- link_to(pipeline.ref,
+ ApplicationController.helpers.link_to(pipeline.ref,
project_commits_path(pipeline.project, pipeline.ref),
class: "ref-name")
end
@@ -116,7 +115,7 @@ module Ci
def link_to_merge_request
return unless merge_request_presenter
- link_to(merge_request_presenter.to_reference,
+ ApplicationController.helpers.link_to(merge_request_presenter.to_reference,
project_merge_request_path(merge_request_presenter.project, merge_request_presenter),
class: 'mr-iid')
end
@@ -143,7 +142,7 @@ module Ci
private
def plain_ref_name
- content_tag(:span, pipeline.ref, class: 'ref-name')
+ ApplicationController.helpers.content_tag(:span, pipeline.ref, class: 'ref-name')
end
def merge_request_presenter
@@ -160,7 +159,7 @@ module Ci
all_related_merge_requests.first(limit).map do |merge_request|
mr_path = project_merge_request_path(merge_request.project, merge_request)
- link_to "#{merge_request.to_reference} #{merge_request.title}", mr_path, class: 'mr-iid'
+ ApplicationController.helpers.link_to "#{merge_request.to_reference} #{merge_request.title}", mr_path, class: 'mr-iid'
end
end
diff --git a/app/presenters/prometheus_alert_presenter.rb b/app/presenters/prometheus_alert_presenter.rb
index 714329ede71..776e2baebdd 100644
--- a/app/presenters/prometheus_alert_presenter.rb
+++ b/app/presenters/prometheus_alert_presenter.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class PrometheusAlertPresenter < Gitlab::View::Presenter::Delegated
- include ActionView::Helpers::UrlHelper
-
presents ::PrometheusAlert, as: :prometheus_alert
def humanized_text
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index bc734465750..ea4723c9e28 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -156,7 +156,7 @@ module Auth
return if path.has_repository?
return unless actions.include?('push')
- ContainerRepository.create_from_path!(path)
+ ContainerRepository.find_or_create_from_path(path)
end
# Overridden in EE
diff --git a/app/services/ci/process_sync_events_service.rb b/app/services/ci/process_sync_events_service.rb
new file mode 100644
index 00000000000..6be8c41dc6a
--- /dev/null
+++ b/app/services/ci/process_sync_events_service.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Ci
+ class ProcessSyncEventsService
+ include Gitlab::Utils::StrongMemoize
+ include ExclusiveLeaseGuard
+
+ BATCH_SIZE = 1000
+
+ def initialize(sync_event_class, sync_class)
+ @sync_event_class = sync_event_class
+ @sync_class = sync_class
+ end
+
+ def execute
+ return unless ::Feature.enabled?(:ci_namespace_project_mirrors, default_enabled: :yaml)
+
+ # preventing parallel processing over the same event table
+ try_obtain_lease { process_events }
+
+ enqueue_worker_if_there_still_event
+ end
+
+ private
+
+ def process_events
+ events = @sync_event_class.preload_synced_relation.first(BATCH_SIZE)
+
+ return if events.empty?
+
+ first = events.first
+ last_processed = nil
+
+ begin
+ events.each do |event|
+ @sync_class.sync!(event)
+
+ last_processed = event
+ end
+ ensure
+ # remove events till the one that was last succesfully processed
+ @sync_event_class.id_in(first.id..last_processed.id).delete_all if last_processed
+ end
+ end
+
+ def enqueue_worker_if_there_still_event
+ @sync_event_class.enqueue_worker if @sync_event_class.exists?
+ end
+
+ def lease_key
+ "#{super}::#{@sync_event_class}"
+ end
+
+ def lease_timeout
+ 1.minute
+ end
+ end
+end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 3bf6644bb29..be21ed5b73d 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -7,7 +7,7 @@ module Ci
allow_failure stage stage_id stage_idx trigger_request
yaml_variables when environment coverage_regex
description tag_list protected needs_attributes
- resource_group scheduling_type].freeze
+ job_variables_attributes resource_group scheduling_type].freeze
end
def self.extra_accessors
@@ -68,13 +68,7 @@ module Ci
end
def build_attributes(build)
- clone_attributes = if ::Feature.enabled?(:clone_job_variables_at_job_retry, build.project, default_enabled: :yaml)
- self.class.clone_accessors + [:job_variables_attributes]
- else
- self.class.clone_accessors
- end
-
- attributes = clone_attributes.to_h do |attribute|
+ attributes = self.class.clone_accessors.to_h do |attribute|
[attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 9700daf1c4b..171d52c328d 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -80,7 +80,7 @@ class SearchService
def abuse_messages
return [] unless params.abusive?
- params.abuse_detection.errors.messages
+ params.abuse_detection.errors.full_messages
end
def valid_request?
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index c66ba5ba2e1..a1e94172ec3 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -79,7 +79,7 @@
%span= milestone.issues_visible_to_user(current_user).count
.title.hide-collapsed
= s_('MilestoneSidebar|Issues')
- %span.badge.badge-muted.badge-pill.gl-badge.sm= milestone.issues_visible_to_user(current_user).count
+ = gl_badge_tag milestone.issues_visible_to_user(current_user).count, variant: :muted, size: :sm
- if show_new_issue_link?(project)
= link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "float-right", title: s_('MilestoneSidebar|New Issue') do
= s_('MilestoneSidebar|New issue')
@@ -111,7 +111,7 @@
%span= milestone.merge_requests.count
.title.hide-collapsed
= s_('MilestoneSidebar|Merge requests')
- %span.badge.badge-muted.badge-pill.gl-badge.sm= milestone.merge_requests.count
+ = gl_badge_tag milestone.merge_requests.count, variant: :muted, size: :sm
.value.hide-collapsed.bold
- if !project || can?(current_user, :read_merge_request, project)
%span.milestone-stat
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 89a4d9dc7cf..e84646047e4 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -2492,6 +2492,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: namespaces_process_sync_events
+ :worker_name: Namespaces::ProcessSyncEventsWorker
+ :feature_category: :sharding
+ :has_external_dependencies:
+ :urgency: :high
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: new_issue
:worker_name: NewIssueWorker
:feature_category: :team_planning
@@ -2663,6 +2672,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: projects_process_sync_events
+ :worker_name: Projects::ProcessSyncEventsWorker
+ :feature_category: :sharding
+ :has_external_dependencies:
+ :urgency: :high
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: projects_schedule_bulk_repository_shard_moves
:worker_name: Projects::ScheduleBulkRepositoryShardMovesWorker
:feature_category: :gitaly
diff --git a/app/workers/namespaces/process_sync_events_worker.rb b/app/workers/namespaces/process_sync_events_worker.rb
new file mode 100644
index 00000000000..f3c4f5bebb1
--- /dev/null
+++ b/app/workers/namespaces/process_sync_events_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Namespaces
+ # This worker can be called multiple times at the same time but only one of them can
+ # process events at a time. This is ensured by `try_obtain_lease` in `Ci::ProcessSyncEventsService`.
+ # `until_executing` here is to reduce redundant worker enqueuing.
+ class ProcessSyncEventsWorker
+ include ApplicationWorker
+
+ data_consistency :always
+
+ feature_category :sharding
+ urgency :high
+
+ idempotent!
+ deduplicate :until_executing
+
+ def perform
+ ::Ci::ProcessSyncEventsService.new(::Namespaces::SyncEvent, ::Ci::NamespaceMirror).execute
+ end
+ end
+end
diff --git a/app/workers/projects/process_sync_events_worker.rb b/app/workers/projects/process_sync_events_worker.rb
new file mode 100644
index 00000000000..b7c4b4de3d0
--- /dev/null
+++ b/app/workers/projects/process_sync_events_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Projects
+ # This worker can be called multiple times at the same time but only one of them can
+ # process events at a time. This is ensured by `try_obtain_lease` in `Ci::ProcessSyncEventsService`.
+ # `until_executing` here is to reduce redundant worker enqueuing.
+ class ProcessSyncEventsWorker
+ include ApplicationWorker
+
+ data_consistency :always
+
+ feature_category :sharding
+ urgency :high
+
+ idempotent!
+ deduplicate :until_executing
+
+ def perform
+ ::Ci::ProcessSyncEventsService.new(::Projects::SyncEvent, ::Ci::ProjectMirror).execute
+ end
+ end
+end
diff --git a/config/feature_flags/development/api_v3_commits_skip_diff_files.yml b/config/feature_flags/development/api_v3_commits_skip_diff_files.yml
deleted file mode 100644
index a3a953e983c..00000000000
--- a/config/feature_flags/development/api_v3_commits_skip_diff_files.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: api_v3_commits_skip_diff_files
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67647
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344617
-milestone: '14.5'
-type: development
-group: group::integrations
-default_enabled: true
diff --git a/config/feature_flags/development/clone_job_variables_at_job_retry.yml b/config/feature_flags/development/ci_namespace_project_mirrors.yml
index bbb39cf2d77..a2d674c3770 100644
--- a/config/feature_flags/development/clone_job_variables_at_job_retry.yml
+++ b/config/feature_flags/development/ci_namespace_project_mirrors.yml
@@ -1,8 +1,8 @@
---
-name: clone_job_variables_at_job_retry
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75720
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347156
+name: ci_namespace_project_mirrors
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75517
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346786
milestone: '14.6'
type: development
-group: group::pipeline authoring
+group: group::sharding
default_enabled: false
diff --git a/config/feature_flags/development/lfk_automatic_partition_creation.yml b/config/feature_flags/development/lfk_automatic_partition_creation.yml
new file mode 100644
index 00000000000..72678ff9cbf
--- /dev/null
+++ b/config/feature_flags/development/lfk_automatic_partition_creation.yml
@@ -0,0 +1,8 @@
+---
+name: lfk_automatic_partition_creation
+introduced_by_url:
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346907
+milestone: '14.6'
+type: development
+group: group::sharding
+default_enabled: false
diff --git a/config/feature_flags/development/lfk_automatic_partition_dropping.yml b/config/feature_flags/development/lfk_automatic_partition_dropping.yml
new file mode 100644
index 00000000000..5b908a3309e
--- /dev/null
+++ b/config/feature_flags/development/lfk_automatic_partition_dropping.yml
@@ -0,0 +1,8 @@
+---
+name: lfk_automatic_partition_dropping
+introduced_by_url:
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346908
+milestone: '14.6'
+type: development
+group: group::sharding
+default_enabled: false
diff --git a/config/initializers/postgres_partitioning.rb b/config/initializers/postgres_partitioning.rb
index 5af8cf52656..f99333f7c82 100644
--- a/config/initializers/postgres_partitioning.rb
+++ b/config/initializers/postgres_partitioning.rb
@@ -2,7 +2,8 @@
Gitlab::Database::Partitioning.register_models([
AuditEvent,
- WebHookLog
+ WebHookLog,
+ LooseForeignKeys::DeletedRecord
])
if Gitlab.ee?
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index d03d6fbd30f..49989e022fa 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -273,6 +273,8 @@
- 1
- - namespaces_onboarding_user_added
- 1
+- - namespaces_process_sync_events
+ - 1
- - namespaces_sync_namespace_name
- 1
- - new_epic
@@ -339,6 +341,8 @@
- 1
- - projects_post_creation
- 1
+- - projects_process_sync_events
+ - 1
- - projects_schedule_bulk_repository_shard_moves
- 1
- - projects_update_repository_storage
diff --git a/db/fixtures/development/32_crm.rb b/db/fixtures/development/32_crm.rb
new file mode 100644
index 00000000000..4a2abfed3ac
--- /dev/null
+++ b/db/fixtures/development/32_crm.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+class Gitlab::Seeder::Crm
+ attr_reader :group, :organizations_per_group, :contacts_per_group
+
+ def initialize(group, organizations_per_group: 10, contacts_per_group: 40)
+ @group = group
+ @organizations_per_group = organizations_per_group
+ @contacts_per_group = contacts_per_group
+ end
+
+ def seed!
+ organization_ids = []
+
+ organizations_per_group.times do
+ organization_ids << ::CustomerRelations::Organization.create!(
+ group_id: group.id,
+ name: FFaker::Company.name
+ ).id
+
+ print '.'
+ end
+
+ contacts_per_group.times do |index|
+ first_name = FFaker::Name.first_name
+ last_name = FFaker::Name.last_name
+ organization_id = index % 3 == 0 ? organization_ids.sample : nil
+ ::CustomerRelations::Contact.create!(
+ group_id: group.id,
+ first_name: first_name,
+ last_name: last_name,
+ email: "#{first_name}.#{last_name}@example.org",
+ organization_id: organization_id
+ )
+
+ print '.'
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ puts "\nGenerating group crm organizations and contacts"
+
+ Group.all.find_each do |group|
+ Gitlab::Seeder::Crm.new(group).seed!
+ end
+end
diff --git a/db/migrate/20211011140932_create_namespaces_sync_events.rb b/db/migrate/20211011140932_create_namespaces_sync_events.rb
new file mode 100644
index 00000000000..06831423343
--- /dev/null
+++ b/db/migrate/20211011140932_create_namespaces_sync_events.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class CreateNamespacesSyncEvents < Gitlab::Database::Migration[1.0]
+ def change
+ create_table :namespaces_sync_events do |t|
+ t.references :namespace, null: false, index: true, foreign_key: { on_delete: :cascade }
+ end
+ end
+end
diff --git a/db/migrate/20211011141239_create_projects_sync_events.rb b/db/migrate/20211011141239_create_projects_sync_events.rb
new file mode 100644
index 00000000000..50fe988ac1b
--- /dev/null
+++ b/db/migrate/20211011141239_create_projects_sync_events.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class CreateProjectsSyncEvents < Gitlab::Database::Migration[1.0]
+ def change
+ create_table :projects_sync_events do |t|
+ t.references :project, null: false, index: true, foreign_key: { on_delete: :cascade }
+ end
+ end
+end
diff --git a/db/migrate/20211011141242_create_namespaces_sync_trigger.rb b/db/migrate/20211011141242_create_namespaces_sync_trigger.rb
new file mode 100644
index 00000000000..91f64709f28
--- /dev/null
+++ b/db/migrate/20211011141242_create_namespaces_sync_trigger.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class CreateNamespacesSyncTrigger < Gitlab::Database::Migration[1.0]
+ include Gitlab::Database::SchemaHelpers
+
+ enable_lock_retries!
+
+ TABLE_NAME = 'namespaces'
+ EVENT_TABLE_NAME = 'namespaces_sync_events'
+ FUNCTION_NAME = 'insert_namespaces_sync_event'
+ TRIGGER_ON_INSERT = 'trigger_namespaces_parent_id_on_insert'
+ TRIGGER_ON_UPDATE = 'trigger_namespaces_parent_id_on_update'
+
+ def up
+ create_trigger_function(FUNCTION_NAME) do
+ <<~SQL
+ INSERT INTO #{EVENT_TABLE_NAME} (namespace_id)
+ VALUES(COALESCE(NEW.id, OLD.id));
+ RETURN NULL;
+ SQL
+ end
+
+ create_trigger(TABLE_NAME, TRIGGER_ON_INSERT, FUNCTION_NAME, fires: 'AFTER INSERT')
+
+ create_trigger(TABLE_NAME, TRIGGER_ON_UPDATE, FUNCTION_NAME, fires: 'AFTER UPDATE') do
+ <<~SQL
+ WHEN (OLD.parent_id IS DISTINCT FROM NEW.parent_id)
+ SQL
+ end
+ end
+
+ def down
+ drop_trigger(TABLE_NAME, TRIGGER_ON_INSERT)
+ drop_trigger(TABLE_NAME, TRIGGER_ON_UPDATE)
+ drop_function(FUNCTION_NAME)
+ end
+end
diff --git a/db/migrate/20211011141243_create_projects_sync_trigger.rb b/db/migrate/20211011141243_create_projects_sync_trigger.rb
new file mode 100644
index 00000000000..03b31c35a3a
--- /dev/null
+++ b/db/migrate/20211011141243_create_projects_sync_trigger.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class CreateProjectsSyncTrigger < Gitlab::Database::Migration[1.0]
+ include Gitlab::Database::SchemaHelpers
+
+ enable_lock_retries!
+
+ TABLE_NAME = 'projects'
+ EVENT_TABLE_NAME = 'projects_sync_events'
+ FUNCTION_NAME = 'insert_projects_sync_event'
+ TRIGGER_ON_INSERT = 'trigger_projects_parent_id_on_insert'
+ TRIGGER_ON_UPDATE = 'trigger_projects_parent_id_on_update'
+
+ def up
+ create_trigger_function(FUNCTION_NAME) do
+ <<~SQL
+ INSERT INTO #{EVENT_TABLE_NAME} (project_id)
+ VALUES(COALESCE(NEW.id, OLD.id));
+ RETURN NULL;
+ SQL
+ end
+
+ create_trigger(TABLE_NAME, TRIGGER_ON_INSERT, FUNCTION_NAME, fires: 'AFTER INSERT')
+
+ create_trigger(TABLE_NAME, TRIGGER_ON_UPDATE, FUNCTION_NAME, fires: 'AFTER UPDATE') do
+ <<~SQL
+ WHEN (OLD.namespace_id IS DISTINCT FROM NEW.namespace_id)
+ SQL
+ end
+ end
+
+ def down
+ drop_trigger(TABLE_NAME, TRIGGER_ON_INSERT)
+ drop_trigger(TABLE_NAME, TRIGGER_ON_UPDATE)
+ drop_function(FUNCTION_NAME)
+ end
+end
diff --git a/db/migrate/20211202094944_move_loose_fk_deleted_records_to_dynamic_schema.rb b/db/migrate/20211202094944_move_loose_fk_deleted_records_to_dynamic_schema.rb
new file mode 100644
index 00000000000..84bc551d2b5
--- /dev/null
+++ b/db/migrate/20211202094944_move_loose_fk_deleted_records_to_dynamic_schema.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class MoveLooseFkDeletedRecordsToDynamicSchema < Gitlab::Database::Migration[1.0]
+ enable_lock_retries!
+
+ def up
+ if table_exists?('gitlab_partitions_static.loose_foreign_keys_deleted_records_1')
+ execute 'ALTER TABLE gitlab_partitions_static.loose_foreign_keys_deleted_records_1 SET SCHEMA gitlab_partitions_dynamic'
+ end
+ end
+
+ def down
+ if table_exists?('gitlab_partitions_dynamic.loose_foreign_keys_deleted_records_1')
+ execute 'ALTER TABLE gitlab_partitions_dynamic.loose_foreign_keys_deleted_records_1 SET SCHEMA gitlab_partitions_static'
+ end
+ end
+end
diff --git a/db/schema_migrations/20211011140932 b/db/schema_migrations/20211011140932
new file mode 100644
index 00000000000..af0e000b9f3
--- /dev/null
+++ b/db/schema_migrations/20211011140932
@@ -0,0 +1 @@
+0209db1e7be48bcbf0e52b451d37da0ef2ecadd567cdfa47907fc5032c258a27 \ No newline at end of file
diff --git a/db/schema_migrations/20211011141239 b/db/schema_migrations/20211011141239
new file mode 100644
index 00000000000..f215f234a7e
--- /dev/null
+++ b/db/schema_migrations/20211011141239
@@ -0,0 +1 @@
+bc0ae055b331801fbe020c12a66e4e6ae790780121bfd66fd161093c94c7a84a \ No newline at end of file
diff --git a/db/schema_migrations/20211011141242 b/db/schema_migrations/20211011141242
new file mode 100644
index 00000000000..01d082a4bc8
--- /dev/null
+++ b/db/schema_migrations/20211011141242
@@ -0,0 +1 @@
+9fd4977cdb57df827fe1a01f55a305d832ee4240d40af9396e093e3b4dbd1e33 \ No newline at end of file
diff --git a/db/schema_migrations/20211011141243 b/db/schema_migrations/20211011141243
new file mode 100644
index 00000000000..cb2df22b8d7
--- /dev/null
+++ b/db/schema_migrations/20211011141243
@@ -0,0 +1 @@
+b3ce6aa41c70cdcf8637a94c3d4d4e97730899221530f5507c4581aaf2fc3a6c \ No newline at end of file
diff --git a/db/schema_migrations/20211202094944 b/db/schema_migrations/20211202094944
new file mode 100644
index 00000000000..b917cca67fa
--- /dev/null
+++ b/db/schema_migrations/20211202094944
@@ -0,0 +1 @@
+2bca61880005c9303b2ff71747cde64d3418b6ef8ad2a9f114d584f4149e386b \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index d73bb531e96..3545280f16f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -34,6 +34,28 @@ BEGIN
END
$$;
+CREATE FUNCTION insert_namespaces_sync_event() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+BEGIN
+INSERT INTO namespaces_sync_events (namespace_id)
+VALUES(COALESCE(NEW.id, OLD.id));
+RETURN NULL;
+
+END
+$$;
+
+CREATE FUNCTION insert_projects_sync_event() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+BEGIN
+INSERT INTO projects_sync_events (project_id)
+VALUES(COALESCE(NEW.id, OLD.id));
+RETURN NULL;
+
+END
+$$;
+
CREATE FUNCTION integrations_set_type_new() RETURNS trigger
LANGUAGE plpgsql
AS $$
@@ -125,6 +147,18 @@ CREATE TABLE incident_management_pending_issue_escalations (
)
PARTITION BY RANGE (process_at);
+CREATE TABLE loose_foreign_keys_deleted_records (
+ id bigint NOT NULL,
+ partition bigint DEFAULT 1 NOT NULL,
+ primary_key_value bigint NOT NULL,
+ status smallint DEFAULT 1 NOT NULL,
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
+ fully_qualified_table_name text NOT NULL,
+ consume_after timestamp with time zone DEFAULT now(),
+ CONSTRAINT check_1a541f3235 CHECK ((char_length(fully_qualified_table_name) <= 150))
+)
+PARTITION BY LIST (partition);
+
CREATE TABLE verification_codes (
created_at timestamp with time zone DEFAULT now() NOT NULL,
visitor_id_code text NOT NULL,
@@ -1013,39 +1047,6 @@ CREATE TABLE gitlab_partitions_static.analytics_cycle_analytics_merge_request_st
);
ALTER TABLE ONLY analytics_cycle_analytics_merge_request_stage_events ATTACH PARTITION gitlab_partitions_static.analytics_cycle_analytics_merge_request_stage_events_31 FOR VALUES WITH (modulus 32, remainder 31);
-CREATE TABLE loose_foreign_keys_deleted_records (
- id bigint NOT NULL,
- partition bigint DEFAULT 1 NOT NULL,
- primary_key_value bigint NOT NULL,
- status smallint DEFAULT 1 NOT NULL,
- created_at timestamp with time zone DEFAULT now() NOT NULL,
- fully_qualified_table_name text NOT NULL,
- consume_after timestamp with time zone DEFAULT now(),
- CONSTRAINT check_1a541f3235 CHECK ((char_length(fully_qualified_table_name) <= 150))
-)
-PARTITION BY LIST (partition);
-
-CREATE SEQUENCE loose_foreign_keys_deleted_records_id_seq
- START WITH 1
- INCREMENT BY 1
- NO MINVALUE
- NO MAXVALUE
- CACHE 1;
-
-ALTER SEQUENCE loose_foreign_keys_deleted_records_id_seq OWNED BY loose_foreign_keys_deleted_records.id;
-
-CREATE TABLE gitlab_partitions_static.loose_foreign_keys_deleted_records_1 (
- id bigint DEFAULT nextval('loose_foreign_keys_deleted_records_id_seq'::regclass) NOT NULL,
- partition bigint DEFAULT 1 NOT NULL,
- primary_key_value bigint NOT NULL,
- status smallint DEFAULT 1 NOT NULL,
- created_at timestamp with time zone DEFAULT now() NOT NULL,
- fully_qualified_table_name text NOT NULL,
- consume_after timestamp with time zone DEFAULT now(),
- CONSTRAINT check_1a541f3235 CHECK ((char_length(fully_qualified_table_name) <= 150))
-);
-ALTER TABLE ONLY loose_foreign_keys_deleted_records ATTACH PARTITION gitlab_partitions_static.loose_foreign_keys_deleted_records_1 FOR VALUES IN ('1');
-
CREATE TABLE product_analytics_events_experimental (
id bigint NOT NULL,
project_id integer NOT NULL,
@@ -15867,6 +15868,15 @@ CREATE SEQUENCE lists_id_seq
ALTER SEQUENCE lists_id_seq OWNED BY lists.id;
+CREATE SEQUENCE loose_foreign_keys_deleted_records_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE loose_foreign_keys_deleted_records_id_seq OWNED BY loose_foreign_keys_deleted_records.id;
+
CREATE TABLE member_tasks (
id bigint NOT NULL,
member_id bigint NOT NULL,
@@ -16492,6 +16502,20 @@ CREATE SEQUENCE namespaces_id_seq
ALTER SEQUENCE namespaces_id_seq OWNED BY namespaces.id;
+CREATE TABLE namespaces_sync_events (
+ id bigint NOT NULL,
+ namespace_id bigint NOT NULL
+);
+
+CREATE SEQUENCE namespaces_sync_events_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE namespaces_sync_events_id_seq OWNED BY namespaces_sync_events.id;
+
CREATE TABLE note_diff_files (
id integer NOT NULL,
diff_note_id integer NOT NULL,
@@ -18546,6 +18570,20 @@ CREATE SEQUENCE projects_id_seq
ALTER SEQUENCE projects_id_seq OWNED BY projects.id;
+CREATE TABLE projects_sync_events (
+ id bigint NOT NULL,
+ project_id bigint NOT NULL
+);
+
+CREATE SEQUENCE projects_sync_events_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE projects_sync_events_id_seq OWNED BY projects_sync_events.id;
+
CREATE TABLE prometheus_alert_events (
id bigint NOT NULL,
project_id integer NOT NULL,
@@ -21811,6 +21849,8 @@ ALTER TABLE ONLY namespace_statistics ALTER COLUMN id SET DEFAULT nextval('names
ALTER TABLE ONLY namespaces ALTER COLUMN id SET DEFAULT nextval('namespaces_id_seq'::regclass);
+ALTER TABLE ONLY namespaces_sync_events ALTER COLUMN id SET DEFAULT nextval('namespaces_sync_events_id_seq'::regclass);
+
ALTER TABLE ONLY note_diff_files ALTER COLUMN id SET DEFAULT nextval('note_diff_files_id_seq'::regclass);
ALTER TABLE ONLY notes ALTER COLUMN id SET DEFAULT nextval('notes_id_seq'::regclass);
@@ -21961,6 +22001,8 @@ ALTER TABLE ONLY project_tracing_settings ALTER COLUMN id SET DEFAULT nextval('p
ALTER TABLE ONLY projects ALTER COLUMN id SET DEFAULT nextval('projects_id_seq'::regclass);
+ALTER TABLE ONLY projects_sync_events ALTER COLUMN id SET DEFAULT nextval('projects_sync_events_id_seq'::regclass);
+
ALTER TABLE ONLY prometheus_alert_events ALTER COLUMN id SET DEFAULT nextval('prometheus_alert_events_id_seq'::regclass);
ALTER TABLE ONLY prometheus_alerts ALTER COLUMN id SET DEFAULT nextval('prometheus_alerts_id_seq'::regclass);
@@ -22397,12 +22439,6 @@ ALTER TABLE ONLY gitlab_partitions_static.analytics_cycle_analytics_merge_reques
ALTER TABLE ONLY gitlab_partitions_static.analytics_cycle_analytics_merge_request_stage_events_31
ADD CONSTRAINT analytics_cycle_analytics_merge_request_stage_events_31_pkey PRIMARY KEY (stage_event_hash_id, merge_request_id);
-ALTER TABLE ONLY loose_foreign_keys_deleted_records
- ADD CONSTRAINT loose_foreign_keys_deleted_records_pkey PRIMARY KEY (partition, id);
-
-ALTER TABLE ONLY gitlab_partitions_static.loose_foreign_keys_deleted_records_1
- ADD CONSTRAINT loose_foreign_keys_deleted_records_1_pkey PRIMARY KEY (partition, id);
-
ALTER TABLE ONLY product_analytics_events_experimental
ADD CONSTRAINT product_analytics_events_experimental_pkey PRIMARY KEY (id, project_id);
@@ -23492,6 +23528,9 @@ ALTER TABLE ONLY list_user_preferences
ALTER TABLE ONLY lists
ADD CONSTRAINT lists_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY loose_foreign_keys_deleted_records
+ ADD CONSTRAINT loose_foreign_keys_deleted_records_pkey PRIMARY KEY (partition, id);
+
ALTER TABLE ONLY member_tasks
ADD CONSTRAINT member_tasks_pkey PRIMARY KEY (id);
@@ -23582,6 +23621,9 @@ ALTER TABLE ONLY namespace_statistics
ALTER TABLE ONLY namespaces
ADD CONSTRAINT namespaces_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY namespaces_sync_events
+ ADD CONSTRAINT namespaces_sync_events_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY note_diff_files
ADD CONSTRAINT note_diff_files_pkey PRIMARY KEY (id);
@@ -23852,6 +23894,9 @@ ALTER TABLE ONLY project_tracing_settings
ALTER TABLE ONLY projects
ADD CONSTRAINT projects_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY projects_sync_events
+ ADD CONSTRAINT projects_sync_events_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY prometheus_alert_events
ADD CONSTRAINT prometheus_alert_events_pkey PRIMARY KEY (id);
@@ -24256,10 +24301,6 @@ CREATE INDEX index_merge_request_stage_events_project_duration ON ONLY analytics
CREATE INDEX index_006f943df6 ON gitlab_partitions_static.analytics_cycle_analytics_merge_request_stage_events_16 USING btree (stage_event_hash_id, project_id, end_event_timestamp, merge_request_id, start_event_timestamp) WHERE (end_event_timestamp IS NOT NULL);
-CREATE INDEX index_loose_foreign_keys_deleted_records_for_partitioned_query ON ONLY loose_foreign_keys_deleted_records USING btree (partition, fully_qualified_table_name, consume_after, id) WHERE (status = 1);
-
-CREATE INDEX index_01e3390fac ON gitlab_partitions_static.loose_foreign_keys_deleted_records_1 USING btree (partition, fully_qualified_table_name, consume_after, id) WHERE (status = 1);
-
CREATE INDEX index_02749b504c ON gitlab_partitions_static.analytics_cycle_analytics_merge_request_stage_events_11 USING btree (stage_event_hash_id, project_id, end_event_timestamp, merge_request_id, start_event_timestamp) WHERE (end_event_timestamp IS NOT NULL);
CREATE INDEX index_merge_request_stage_events_group_duration ON ONLY analytics_cycle_analytics_merge_request_stage_events USING btree (stage_event_hash_id, group_id, end_event_timestamp, merge_request_id, start_event_timestamp) WHERE (end_event_timestamp IS NOT NULL);
@@ -26514,6 +26555,8 @@ CREATE INDEX index_lists_on_milestone_id ON lists USING btree (milestone_id);
CREATE INDEX index_lists_on_user_id ON lists USING btree (user_id);
+CREATE INDEX index_loose_foreign_keys_deleted_records_for_partitioned_query ON ONLY loose_foreign_keys_deleted_records USING btree (partition, fully_qualified_table_name, consume_after, id) WHERE (status = 1);
+
CREATE INDEX index_member_tasks_on_member_id ON member_tasks USING btree (member_id);
CREATE UNIQUE INDEX index_member_tasks_on_member_id_and_project_id ON member_tasks USING btree (member_id, project_id);
@@ -26744,6 +26787,8 @@ CREATE INDEX index_namespaces_on_type_and_id ON namespaces USING btree (type, id
CREATE INDEX index_namespaces_public_groups_name_id ON namespaces USING btree (name, id) WHERE (((type)::text = 'Group'::text) AND (visibility_level = 20));
+CREATE INDEX index_namespaces_sync_events_on_namespace_id ON namespaces_sync_events USING btree (namespace_id);
+
CREATE INDEX index_non_requested_project_members_on_source_id_and_type ON members USING btree (source_id, source_type) WHERE ((requested_at IS NULL) AND ((type)::text = 'ProjectMember'::text));
CREATE UNIQUE INDEX index_note_diff_files_on_diff_note_id ON note_diff_files USING btree (diff_note_id);
@@ -27204,6 +27249,8 @@ CREATE INDEX index_projects_on_star_count ON projects USING btree (star_count);
CREATE INDEX index_projects_on_updated_at_and_id ON projects USING btree (updated_at, id);
+CREATE INDEX index_projects_sync_events_on_project_id ON projects_sync_events USING btree (project_id);
+
CREATE UNIQUE INDEX index_prometheus_alert_event_scoped_payload_key ON prometheus_alert_events USING btree (prometheus_alert_id, payload_key);
CREATE INDEX index_prometheus_alert_events_on_project_id_and_status ON prometheus_alert_events USING btree (project_id, status);
@@ -28164,8 +28211,6 @@ ALTER INDEX index_issue_stage_events_project_duration ATTACH PARTITION gitlab_pa
ALTER INDEX index_merge_request_stage_events_project_duration ATTACH PARTITION gitlab_partitions_static.index_006f943df6;
-ALTER INDEX index_loose_foreign_keys_deleted_records_for_partitioned_query ATTACH PARTITION gitlab_partitions_static.index_01e3390fac;
-
ALTER INDEX index_merge_request_stage_events_project_duration ATTACH PARTITION gitlab_partitions_static.index_02749b504c;
ALTER INDEX index_merge_request_stage_events_group_duration ATTACH PARTITION gitlab_partitions_static.index_0287f5ba09;
@@ -28674,8 +28719,6 @@ ALTER INDEX index_issue_stage_events_project_duration ATTACH PARTITION gitlab_pa
ALTER INDEX index_issue_stage_events_group_in_progress_duration ATTACH PARTITION gitlab_partitions_static.index_ff8741d8d7;
-ALTER INDEX loose_foreign_keys_deleted_records_pkey ATTACH PARTITION gitlab_partitions_static.loose_foreign_keys_deleted_records_1_pkey;
-
ALTER INDEX index_product_analytics_events_experimental_project_and_time ATTACH PARTITION gitlab_partitions_static.product_analytics_events_expe_project_id_collector_tstamp_idx10;
ALTER INDEX index_product_analytics_events_experimental_project_and_time ATTACH PARTITION gitlab_partitions_static.product_analytics_events_expe_project_id_collector_tstamp_idx11;
@@ -28960,6 +29003,14 @@ CREATE TRIGGER trigger_has_external_wiki_on_type_new_updated AFTER UPDATE OF typ
CREATE TRIGGER trigger_has_external_wiki_on_update AFTER UPDATE ON integrations FOR EACH ROW WHEN (((new.type_new = 'Integrations::ExternalWiki'::text) AND (old.active <> new.active) AND (new.project_id IS NOT NULL))) EXECUTE FUNCTION set_has_external_wiki();
+CREATE TRIGGER trigger_namespaces_parent_id_on_insert AFTER INSERT ON namespaces FOR EACH ROW EXECUTE FUNCTION insert_namespaces_sync_event();
+
+CREATE TRIGGER trigger_namespaces_parent_id_on_update AFTER UPDATE ON namespaces FOR EACH ROW WHEN ((old.parent_id IS DISTINCT FROM new.parent_id)) EXECUTE FUNCTION insert_namespaces_sync_event();
+
+CREATE TRIGGER trigger_projects_parent_id_on_insert AFTER INSERT ON projects FOR EACH ROW EXECUTE FUNCTION insert_projects_sync_event();
+
+CREATE TRIGGER trigger_projects_parent_id_on_update AFTER UPDATE ON projects FOR EACH ROW WHEN ((old.namespace_id IS DISTINCT FROM new.namespace_id)) EXECUTE FUNCTION insert_projects_sync_event();
+
CREATE TRIGGER trigger_type_new_on_insert AFTER INSERT ON integrations FOR EACH ROW EXECUTE FUNCTION integrations_set_type_new();
ALTER TABLE ONLY chat_names
@@ -30870,6 +30921,9 @@ ALTER TABLE ONLY gpg_keys
ALTER TABLE ONLY analytics_language_trend_repository_languages
ADD CONSTRAINT fk_rails_9d851d566c FOREIGN KEY (programming_language_id) REFERENCES programming_languages(id) ON DELETE CASCADE;
+ALTER TABLE ONLY namespaces_sync_events
+ ADD CONSTRAINT fk_rails_9da32a0431 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY badges
ADD CONSTRAINT fk_rails_9df4a56538 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@@ -31044,6 +31098,9 @@ ALTER TABLE ONLY security_findings
ALTER TABLE ONLY packages_debian_project_component_files
ADD CONSTRAINT fk_rails_bbe9ebfbd9 FOREIGN KEY (component_id) REFERENCES packages_debian_project_components(id) ON DELETE RESTRICT;
+ALTER TABLE ONLY projects_sync_events
+ ADD CONSTRAINT fk_rails_bbf0eef59f FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY approval_merge_request_rules_users
ADD CONSTRAINT fk_rails_bc8972fa55 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
diff --git a/doc/administration/gitaly/recovery.md b/doc/administration/gitaly/recovery.md
index 81e655da44b..3c4fd276187 100644
--- a/doc/administration/gitaly/recovery.md
+++ b/doc/administration/gitaly/recovery.md
@@ -259,7 +259,7 @@ replication job is scheduled only if there are no other replication jobs pending
repository.
The reconciliation frequency can be changed via the configuration. The value can be any valid
-[Go duration value](https://golang.org/pkg/time/#ParseDuration). Values below 0 disable the feature.
+[Go duration value](https://pkg.go.dev/time#ParseDuration). Values below 0 disable the feature.
Examples:
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index ec9e3e41ca1..9457d629425 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -799,7 +799,7 @@ Incorrect configuration of these values may result in intermittent
or persistent errors, or the Pages Daemon serving old content.
NOTE:
-Expiry, interval and timeout flags use [Golang's duration formatting](https://golang.org/pkg/time/#ParseDuration).
+Expiry, interval and timeout flags use [Golang's duration formatting](https://pkg.go.dev/time#ParseDuration).
A duration string is a possibly signed sequence of decimal numbers,
each with optional fraction and a unit suffix, such as `300ms`, `1.5h` or `2h45m`.
Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`.
@@ -1286,7 +1286,7 @@ in all of your GitLab Pages instances.
### 500 error with `securecookie: failed to generate random iv` and `Failed to save the session`
This problem most likely results from an [out-dated operating system](../package_information/supported_os.md#os-versions-that-are-no-longer-supported).
-The [Pages daemon uses the `securecookie` library](https://gitlab.com/search?group_id=9970&project_id=734943&repository_ref=master&scope=blobs&search=securecookie&snippets=false) to get random strings via [`crypto/rand` in Go](https://golang.org/pkg/crypto/rand/#pkg-variables).
+The [Pages daemon uses the `securecookie` library](https://gitlab.com/search?group_id=9970&project_id=734943&repository_ref=master&scope=blobs&search=securecookie&snippets=false) to get random strings via [`crypto/rand` in Go](https://pkg.go.dev/crypto/rand#pkg-variables).
This requires the `getrandom` system call or `/dev/urandom` to be available on the host OS.
Upgrading to an [officially supported operating system](https://about.gitlab.com/install/) is recommended.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index a3a26c75b79..e0b93119042 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -3758,7 +3758,7 @@ Input type: `ProjectSetComplianceFrameworkInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationprojectsetcomplianceframeworkclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationprojectsetcomplianceframeworkcomplianceframeworkid"></a>`complianceFrameworkId` | [`ComplianceManagementFrameworkID`](#compliancemanagementframeworkid) | ID of the compliance framework to assign to the project. |
+| <a id="mutationprojectsetcomplianceframeworkcomplianceframeworkid"></a>`complianceFrameworkId` | [`ComplianceManagementFrameworkID`](#compliancemanagementframeworkid) | ID of the compliance framework to assign to the project. Set to `null` to unset. |
| <a id="mutationprojectsetcomplianceframeworkprojectid"></a>`projectId` | [`ProjectID!`](#projectid) | ID of the project to change the compliance framework of. |
#### Fields
diff --git a/doc/api/topics.md b/doc/api/topics.md
index 5e9e1b8fc12..3b0421d8f25 100644
--- a/doc/api/topics.md
+++ b/doc/api/topics.md
@@ -161,7 +161,7 @@ curl --request PUT \
--data "name=topic1" \
--header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/topics/1"
-
+```
Example response:
diff --git a/doc/architecture/blueprints/cloud_native_gitlab_pages/index.md b/doc/architecture/blueprints/cloud_native_gitlab_pages/index.md
index 60ddfe8ce02..e545e8844ec 100644
--- a/doc/architecture/blueprints/cloud_native_gitlab_pages/index.md
+++ b/doc/architecture/blueprints/cloud_native_gitlab_pages/index.md
@@ -28,7 +28,7 @@ might be useful to understand why it is important, and what is the roadmap.
## How GitLab Pages Works
GitLab Pages is a daemon designed to serve static content, written in
-[Go](https://golang.org/).
+[Go](https://go.dev/).
Initially, GitLab Pages has been designed to store static content on a local
shared block storage (NFS) in a hierarchical group > project directory
diff --git a/doc/architecture/blueprints/container_registry_metadata_database/index.md b/doc/architecture/blueprints/container_registry_metadata_database/index.md
index 7bbaefb8e1e..a38a8727dc4 100644
--- a/doc/architecture/blueprints/container_registry_metadata_database/index.md
+++ b/doc/architecture/blueprints/container_registry_metadata_database/index.md
@@ -18,7 +18,7 @@ For GitLab.com and for GitLab customers, the Container Registry is a critical co
## Current Architecture
-The Container Registry is a single [Go](https://golang.org/) application. Its only dependency is the storage backend on which images and metadata are stored.
+The Container Registry is a single [Go](https://go.dev/) application. Its only dependency is the storage backend on which images and metadata are stored.
```mermaid
graph LR
@@ -146,7 +146,7 @@ The interaction between the registry and its clients, including GitLab Rails and
### Database
-Following the GitLab [Go standards and style guidelines](../../../development/go_guide), no ORM is used to manage the database, only the [`database/sql`](https://golang.org/pkg/database/sql/) package from the Go standard library, a PostgreSQL driver ([`lib/pq`](https://pkg.go.dev/github.com/lib/pq?tab=doc)) and raw SQL queries, over a TCP connection pool.
+Following the GitLab [Go standards and style guidelines](../../../development/go_guide), no ORM is used to manage the database, only the [`database/sql`](https://pkg.go.dev/database/sql) package from the Go standard library, a PostgreSQL driver ([`lib/pq`](https://pkg.go.dev/github.com/lib/pq?tab=doc)) and raw SQL queries, over a TCP connection pool.
The design and development of the registry database adhere to the GitLab [database guidelines](../../../development/database/). Being a Go application, the required tooling to support the database will have to be developed, such as for running database migrations.
diff --git a/doc/ci/ci_cd_for_external_repos/index.md b/doc/ci/ci_cd_for_external_repos/index.md
index 4012a8ae55b..7bc138d083d 100644
--- a/doc/ci/ci_cd_for_external_repos/index.md
+++ b/doc/ci/ci_cd_for_external_repos/index.md
@@ -11,7 +11,7 @@ type: index, howto
INFO:
Get external repo access and more by upgrading to GitLab Ultimate.
-[Try a free 30-day trial now](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=p-ci-cd-external-docs).
+[Try a free 30-day trial now](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=p-ci-cd-external-docs).
GitLab CI/CD can be used with [GitHub](github_integration.md), [Bitbucket Cloud](bitbucket_integration.md), or any other
Git server.
diff --git a/doc/ci/pipelines/merge_trains.md b/doc/ci/pipelines/merge_trains.md
index f9e4f3fbdea..593cdb68b3f 100644
--- a/doc/ci/pipelines/merge_trains.md
+++ b/doc/ci/pipelines/merge_trains.md
@@ -13,7 +13,7 @@ last_update: 2019-07-03
INFO:
Get merge trains and more in GitLab Ultimate.
-[Try a free 30-day trial now](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=p-ci-cd-external-docs).
+[Try a free 30-day trial now](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=p-ci-cd-external-docs).
For more information about why you might want to use merge trains, read [How merge trains keep your master green](https://about.gitlab.com/blog/2020/01/30/all-aboard-merge-trains/).
diff --git a/doc/ci/pipelines/pipelines_for_merged_results.md b/doc/ci/pipelines/pipelines_for_merged_results.md
index c9e60cc0326..718519faf48 100644
--- a/doc/ci/pipelines/pipelines_for_merged_results.md
+++ b/doc/ci/pipelines/pipelines_for_merged_results.md
@@ -12,7 +12,7 @@ last_update: 2019-07-03
INFO:
Get these pipelines and more in GitLab Ultimate.
-[Try a free 30-day trial now](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=p-ci-cd-external-docs).
+[Try a free 30-day trial now](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=p-ci-cd-external-docs).
When you submit a merge request, you are requesting to merge changes from a
source branch into a target branch. By default, the CI pipeline runs jobs
diff --git a/doc/ci/test_cases/index.md b/doc/ci/test_cases/index.md
index 1c2c071e2a8..4c840125d24 100644
--- a/doc/ci/test_cases/index.md
+++ b/doc/ci/test_cases/index.md
@@ -13,7 +13,7 @@ type: reference
INFO:
Create test cases in GitLab Ultimate.
-[Try it free for 30 days](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=u-test-cases-docs).
+[Try it free for 30 days](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=u-test-cases-docs).
Test cases in GitLab can help your teams create testing scenarios in their existing development platform.
diff --git a/doc/development/go_guide/go_upgrade.md b/doc/development/go_guide/go_upgrade.md
index cde2ee39e3c..889849799bc 100644
--- a/doc/development/go_guide/go_upgrade.md
+++ b/doc/development/go_guide/go_upgrade.md
@@ -32,7 +32,7 @@ Individual Golang projects need to support multiple Go versions because:
- We must support the [official Omnibus GitLab Go version](#updating-go-version), which may be behind the latest minor release.
- When Omnibus switches Go version, we still may need to support the old one for security backports.
-These 3 requirements may easily be satisfied by keeping support for the [3 latest minor versions of Go](https://golang.org/dl/).
+These 3 requirements may easily be satisfied by keeping support for the [3 latest minor versions of Go](https://go.dev/dl/).
It is ok to drop support for the oldest Go version and support only the 2 latest releases,
if this is enough to support backports to the last 3 minor GitLab releases.
@@ -52,12 +52,12 @@ in case of a critical security release.
We should always:
- Use the same Go version for Omnibus GitLab and Cloud Native GitLab.
-- Use a [supported version](https://golang.org/doc/devel/release#policy).
+- Use a [supported version](https://go.dev/doc/devel/release#policy).
- Use the most recent patch-level for that version to keep up with security fixes.
Changing the version affects every project being compiled, so it's important to
ensure that all projects have been updated to test against the new Go version
-before changing the package builders to use it. Despite [Go's compatibility promise](https://golang.org/doc/go1compat),
+before changing the package builders to use it. Despite [Go's compatibility promise](https://go.dev/doc/go1compat),
changes between minor versions can expose bugs or cause problems in our projects.
### Upgrade process
diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md
index 184605170b8..a5661a77da3 100644
--- a/doc/development/go_guide/index.md
+++ b/doc/development/go_guide/index.md
@@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Go standards and style guidelines
This document describes various guidelines and best practices for GitLab
-projects using the [Go language](https://golang.org).
+projects using the [Go language](https://go.dev/).
## Overview
@@ -103,7 +103,7 @@ projects:
- Use `goimports` before committing.
[`goimports`](https://pkg.go.dev/golang.org/x/tools/cmd/goimports)
is a tool that automatically formats Go source code using
- [`Gofmt`](https://golang.org/cmd/gofmt/), in addition to formatting import lines,
+ [`Gofmt`](https://pkg.go.dev/cmd/gofmt), in addition to formatting import lines,
adding missing ones and removing unreferenced ones.
Most editors/IDEs allow you to run commands before/after saving a file, you can set it
@@ -196,7 +196,7 @@ deploy a new pod, migrating the data automatically.
### Testing frameworks
We should not use any specific library or framework for testing, as the
-[standard library](https://golang.org/pkg/) provides already everything to get
+[standard library](https://pkg.go.dev/std) provides already everything to get
started. If there is a need for more sophisticated testing tools, the following
external dependencies might be worth considering in case we decide to use a specific
library or framework:
@@ -279,7 +279,7 @@ to make the test output easily readable.
### Benchmarks
Programs handling a lot of IO or complex operations should always include
-[benchmarks](https://golang.org/pkg/testing/#hdr-Benchmarks), to ensure
+[benchmarks](https://pkg.go.dev/testing#hdr-Benchmarks), to ensure
performance consistency over time.
## Error handling
@@ -435,7 +435,7 @@ The following are some style guidelines that are specific to the Secure Team.
Use `goimports -local gitlab.com/gitlab-org` before committing.
[`goimports`](https://pkg.go.dev/golang.org/x/tools/cmd/goimports)
is a tool that automatically formats Go source code using
-[`Gofmt`](https://golang.org/cmd/gofmt/), in addition to formatting import lines,
+[`Gofmt`](https://pkg.go.dev/cmd/gofmt), in addition to formatting import lines,
adding missing ones and removing unreferenced ones.
By using the `-local gitlab.com/gitlab-org` option, `goimports` groups locally referenced
packages separately from external ones. See
diff --git a/doc/development/integrations/secure.md b/doc/development/integrations/secure.md
index 34293845d17..356e731aa87 100644
--- a/doc/development/integrations/secure.md
+++ b/doc/development/integrations/secure.md
@@ -298,7 +298,7 @@ this makes it possible to debug the problem without having to change the log lev
#### common `logutil` package
-If you are using [go](https://golang.org/) and
+If you are using [go](https://go.dev/) and
[common](https://gitlab.com/gitlab-org/security-products/analyzers/common),
then it is suggested that you use [Logrus](https://github.com/Sirupsen/logrus)
and [common's `logutil` package](https://gitlab.com/gitlab-org/security-products/analyzers/common/-/tree/master/logutil)
diff --git a/doc/development/secure_coding_guidelines.md b/doc/development/secure_coding_guidelines.md
index 65fdb815f87..9798c551d54 100644
--- a/doc/development/secure_coding_guidelines.md
+++ b/doc/development/secure_coding_guidelines.md
@@ -188,7 +188,7 @@ and [possessive quantifiers](https://www.regular-expressions.info/possessive.htm
#### Go
-Go's [`regexp`](https://golang.org/pkg/regexp/) package uses `re2` and isn't vulnerable to backtracking issues.
+Go's [`regexp`](https://pkg.go.dev/regexp) package uses `re2` and isn't vulnerable to backtracking issues.
## Further Links
diff --git a/doc/development/shell_scripting_guide/index.md b/doc/development/shell_scripting_guide/index.md
index d3b446d45da..3d58fabad72 100644
--- a/doc/development/shell_scripting_guide/index.md
+++ b/doc/development/shell_scripting_guide/index.md
@@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
GitLab consists of many various services and sub-projects. The majority of
their backend code is written in [Ruby](https://www.ruby-lang.org) and
-[Go](https://golang.org). However, some of them use shell scripts for
+[Go](https://go.dev/). However, some of them use shell scripts for
automation of routine system administration tasks like deployment,
installation, etc. It's being done either for historical reasons or as an effort
to minimize the dependencies, for instance, for Docker images.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 51372c48ad0..f405bc40f43 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -244,13 +244,13 @@ sudo make install
GitLab has several daemons written in Go. To install
GitLab we need a Go compiler. The instructions below assume you use 64-bit
Linux. You can find downloads for other platforms at the [Go download
-page](https://golang.org/dl).
+page](https://go.dev/dl).
```shell
# Remove former Go installation folder
sudo rm -rf /usr/local/go
-curl --remote-name --progress-bar "https://golang.org/dl/go1.16.10.linux-amd64.tar.gz"
+curl --remote-name --progress-bar "https://go.dev/dl/go1.16.10.linux-amd64.tar.gz"
echo '414cd18ce1d193769b9e97d2401ad718755ab47816e13b2a1cde203d263b55cf go1.16.10.linux-amd64.tar.gz' | shasum -a256 -c - && \
sudo tar -C /usr/local -xzf go1.16.10.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,gofmt} /usr/local/bin/
diff --git a/doc/push_rules/push_rules.md b/doc/push_rules/push_rules.md
index 6ec6f5f7721..84a6bb4c2a6 100644
--- a/doc/push_rules/push_rules.md
+++ b/doc/push_rules/push_rules.md
@@ -16,7 +16,7 @@ enforcing a special format for commit messages.
INFO:
Get access to push rules and more with a
-[free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=p-push-rules-docs).
+[free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=p-push-rules-docs).
Push rules are [pre-receive Git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) you
can enable in a user-friendly interface. They are defined either:
diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md
index e304f4e1525..6657d729a70 100644
--- a/doc/subscriptions/index.md
+++ b/doc/subscriptions/index.md
@@ -9,7 +9,7 @@ type: index, reference
INFO:
Get advanced search and more with
-[a trial of GitLab Ultimate](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=u-subscription-docs).
+[a trial of GitLab Ultimate](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=u-subscription-docs).
Free for 30 days.
GitLab offers tiers of features. Your subscription determines which tier you
diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md
index 4343b464ba6..22ffcda9138 100644
--- a/doc/update/upgrading_from_source.md
+++ b/doc/update/upgrading_from_source.md
@@ -111,7 +111,7 @@ Download and install Go (for Linux, 64-bit):
# Remove former Go installation folder
sudo rm -rf /usr/local/go
-curl --remote-name --progress-bar "https://golang.org/dl/go1.16.10.linux-amd64.tar.gz"
+curl --remote-name --progress-bar "https://go.dev/dl/go1.16.10.linux-amd64.tar.gz"
echo '414cd18ce1d193769b9e97d2401ad718755ab47816e13b2a1cde203d263b55cf go1.16.10.linux-amd64.tar.gz' | shasum -a256 -c - && \
sudo tar -C /usr/local -xzf go1.16.10.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,gofmt} /usr/local/bin/
diff --git a/doc/user/application_security/api_fuzzing/index.md b/doc/user/application_security/api_fuzzing/index.md
index 749ae93a542..a0f14ea59a1 100644
--- a/doc/user/application_security/api_fuzzing/index.md
+++ b/doc/user/application_security/api_fuzzing/index.md
@@ -14,7 +14,7 @@ miss.
INFO:
Try fuzz testing in GitLab Ultimate.
-[It's free for 30 days](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=u-api-fuzzing-docs).
+[It's free for 30 days](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=u-api-fuzzing-docs).
We recommend that you use fuzz testing in addition to [GitLab Secure](../index.md)'s
other security scanners and your own test processes. If you're using [GitLab CI/CD](../../../ci/index.md),
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 87286a881aa..05db0c5a4d4 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
INFO:
Want to try out container scanning?
-[Get a free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=u-container-scanning-docs).
+[Get a free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=u-container-scanning-docs).
Your application's Docker image may itself be based on Docker images that contain known
vulnerabilities. By including an extra job in your pipeline that scans for those vulnerabilities and
diff --git a/doc/user/application_security/dast/browser_based.md b/doc/user/application_security/dast/browser_based.md
index f63ed2a5079..5d1e57553f4 100644
--- a/doc/user/application_security/dast/browser_based.md
+++ b/doc/user/application_security/dast/browser_based.md
@@ -61,14 +61,14 @@ The browser-based crawler can be configured using CI/CD variables.
| `DAST_BROWSER_NUMBER_OF_BROWSERS` | number | `3` | The maximum number of concurrent browser instances to use. For shared runners on GitLab.com, we recommended a maximum of three. Private runners with more resources may benefit from a higher number, but are likely to produce little benefit after five to seven instances. |
| `DAST_BROWSER_COOKIES` | dictionary | `abtesting_group:3,region:locked` | A cookie name and value to be added to every request. |
| `DAST_BROWSER_LOG` | List of strings | `brows:debug,auth:debug` | A list of modules and their intended log level. |
-| `DAST_BROWSER_NAVIGATION_TIMEOUT` | [Duration string](https://golang.org/pkg/time/#ParseDuration) | `15s` | The maximum amount of time to wait for a browser to navigate from one page to another. |
-| `DAST_BROWSER_ACTION_TIMEOUT` | [Duration string](https://golang.org/pkg/time/#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to complete an action. |
-| `DAST_BROWSER_STABILITY_TIMEOUT` | [Duration string](https://golang.org/pkg/time/#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis. |
-| `DAST_BROWSER_NAVIGATION_STABILITY_TIMEOUT` | [Duration string](https://golang.org/pkg/time/#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after a navigation completes. |
-| `DAST_BROWSER_ACTION_STABILITY_TIMEOUT` | [Duration string](https://golang.org/pkg/time/#ParseDuration) | `800ms` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after completing an action. |
-| `DAST_BROWSER_SEARCH_ELEMENT_TIMEOUT` | [Duration string](https://golang.org/pkg/time/#ParseDuration) | `3s` | The maximum amount of time to allow the browser to search for new elements or navigations. |
-| `DAST_BROWSER_EXTRACT_ELEMENT_TIMEOUT` | [Duration string](https://golang.org/pkg/time/#ParseDuration) | `5s` | The maximum amount of time to allow the browser to extract newly found elements or navigations. |
-| `DAST_BROWSER_ELEMENT_TIMEOUT` | [Duration string](https://golang.org/pkg/time/#ParseDuration) | `600ms` | The maximum amount of time to wait for an element before determining it is ready for analysis. |
+| `DAST_BROWSER_NAVIGATION_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `15s` | The maximum amount of time to wait for a browser to navigate from one page to another. |
+| `DAST_BROWSER_ACTION_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to complete an action. |
+| `DAST_BROWSER_STABILITY_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis. |
+| `DAST_BROWSER_NAVIGATION_STABILITY_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after a navigation completes. |
+| `DAST_BROWSER_ACTION_STABILITY_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `800ms` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after completing an action. |
+| `DAST_BROWSER_SEARCH_ELEMENT_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `3s` | The maximum amount of time to allow the browser to search for new elements or navigations. |
+| `DAST_BROWSER_EXTRACT_ELEMENT_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `5s` | The maximum amount of time to allow the browser to extract newly found elements or navigations. |
+| `DAST_BROWSER_ELEMENT_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `600ms` | The maximum amount of time to wait for an element before determining it is ready for analysis. |
| `DAST_BROWSER_PAGE_READY_SELECTOR` | selector | `css:#page-is-ready` | Selector that when detected as visible on the page, indicates to the analyzer that the page has finished loading and the scan can continue. Note: When this selector is set, but the element is not found, the scanner waits for the period defined in `DAST_BROWSER_STABILITY_TIMEOUT` before continuing the scan. This can significantly increase scanning time if the element is not present on multiple pages within the site. |
The [DAST variables](index.md#available-cicd-variables) `SECURE_ANALYZERS_PREFIX`, `DAST_FULL_SCAN_ENABLED`, `DAST_AUTO_UPDATE_ADDONS`, `DAST_EXCLUDE_RULES`, `DAST_REQUEST_HEADERS`, `DAST_HTML_REPORT`, `DAST_MARKDOWN_REPORT`, `DAST_XML_REPORT`,
@@ -100,7 +100,7 @@ You can manage the trade-off between coverage and scan time with the following m
Due to poor network conditions or heavy application load, the default timeouts may not be applicable to your application.
-Browser-based scans offer the ability to adjust various timeouts to ensure it continues smoothly as it transitions from one page to the next. These values are configured using a [Duration string](https://golang.org/pkg/time/#ParseDuration), which allow you to configure durations with a prefix: `m` for minutes, `s` for seconds, and `ms` for milliseconds.
+Browser-based scans offer the ability to adjust various timeouts to ensure it continues smoothly as it transitions from one page to the next. These values are configured using a [Duration string](https://pkg.go.dev/time#ParseDuration), which allow you to configure durations with a prefix: `m` for minutes, `s` for seconds, and `ms` for milliseconds.
Navigations, or the act of loading a new page, usually require the most amount of time because they are
loading multiple new resources such as JavaScript or CSS files. Depending on the size of these resources, or the speed at which they are returned, the default `DAST_BROWSER_NAVIGATION_TIMEOUT` may not be sufficient.
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 100aac23144..e4f042e7b0a 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -18,7 +18,7 @@ tool [OWASP Zed Attack Proxy](https://www.zaproxy.org/) for analysis.
INFO:
Want to try out security scanning?
-[Try GitLab Ultimate free for 30 days](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=u-dast-docs).
+[Try GitLab Ultimate free for 30 days](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=u-dast-docs).
After DAST creates its report, GitLab evaluates it for discovered
vulnerabilities between the source and target branches. Relevant
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index d0d28cb0879..0e81151bd46 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
INFO:
Try out Dependency Scanning in GitLab Ultimate.
-[It's free for 30 days](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=u-dependency-scanning-docs).
+[It's free for 30 days](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=u-dependency-scanning-docs).
The Dependency Scanning feature can automatically find security vulnerabilities in your
dependencies while you're developing and testing your applications. For example, dependency scanning
@@ -146,7 +146,7 @@ table.supported-languages ul {
<tr>
<td>Go</td>
<td>N/A</td>
- <td><a href="https://golang.org/">Go</a></td>
+ <td><a href="https://go.dev/">Go</a></td>
<td><code>go.sum</code></td>
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
<td>Y</td>
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 4583f666f59..5500f5a10c4 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -18,7 +18,7 @@ actionable information _before_ changes are merged enables you to be proactive.
INFO:
Want to try out security scanning?
-[Try GitLab Ultimate free for 30 days](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=u-application-security-docs).
+[Try GitLab Ultimate free for 30 days](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=u-application-security-docs).
GitLab also provides high-level statistics of vulnerabilities across projects and groups:
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index 122d462ac96..5afbe1ca54e 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
INFO:
Want to try out security scanning?
-[Try GitLab Ultimate free for 30 days](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=u-security-dashboard-docs).
+[Try GitLab Ultimate free for 30 days](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=u-security-dashboard-docs).
GitLab provides a comprehensive set of features for viewing and managing vulnerabilities:
diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md
index 414feeaf2f3..525362cc562 100644
--- a/doc/user/clusters/agent/index.md
+++ b/doc/user/clusters/agent/index.md
@@ -20,7 +20,7 @@ The Agent is installed into the cluster through code, providing you with a fast,
INFO:
Get Network Security Alerts in GitLab by upgrading to Ultimate.
-[Try a free 30-day trial now](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=p-cluster-agent-docs).
+[Try a free 30-day trial now](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=p-cluster-agent-docs).
With GitOps, you can manage containerized clusters and applications from a Git repository that:
diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md
index 143e2badd9d..2a711e9a6c3 100644
--- a/doc/user/compliance/license_compliance/index.md
+++ b/doc/user/compliance/license_compliance/index.md
@@ -16,7 +16,7 @@ is incompatible with yours, then you can deny the use of that license.
INFO:
Try License Compliance scanning to search project dependencies in GitLab Ultimate.
-[It's free for 30 days](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=u-compliance-docs).
+[It's free for 30 days](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=u-compliance-docs).
You can take advantage of License Compliance by either:
@@ -545,24 +545,24 @@ configured to use this as the default `CA_CERT_PATH`.
### Configuring Go projects
To configure [Go modules](https://github.com/golang/go/wiki/Modules)
-based projects, specify [CI/CD variables](https://golang.org/pkg/cmd/go/#hdr-Environment_variables)
+based projects, specify [CI/CD variables](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
in the `license_scanning` job's [variables](#available-cicd-variables) section in `.gitlab-ci.yml`.
-If a project has [vendored](https://golang.org/pkg/cmd/go/#hdr-Vendor_Directories) its modules,
+If a project has [vendored](https://pkg.go.dev/cmd/go#hdr-Vendor_Directories) its modules,
then the combination of the `vendor` directory and `mod.sum` file are used to detect the software
licenses associated with the Go module dependencies.
#### Using private Go registries
-You can use the [`GOPRIVATE`](https://golang.org/pkg/cmd/go/#hdr-Environment_variables)
-and [`GOPROXY`](https://golang.org/pkg/cmd/go/#hdr-Environment_variables)
+You can use the [`GOPRIVATE`](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
+and [`GOPROXY`](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
environment variables to control where modules are sourced from. Alternatively, you can use
-[`go mod vendor`](https://golang.org/ref/mod#tmp_28) to vendor a project's modules.
+[`go mod vendor`](https://go.dev/ref/mod#tmp_28) to vendor a project's modules.
#### Custom root certificates for Go
-You can specify the [`-insecure`](https://golang.org/pkg/cmd/go/internal/get/) flag by exporting the
-[`GOFLAGS`](https://golang.org/cmd/go/#hdr-Environment_variables)
+You can specify the [`-insecure`](https://pkg.go.dev/cmd/go/internal/get) flag by exporting the
+[`GOFLAGS`](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
environment variable. For example:
```yaml
diff --git a/doc/user/group/epics/epic_boards.md b/doc/user/group/epics/epic_boards.md
index 9076bb1594c..1bc1e4d703b 100644
--- a/doc/user/group/epics/epic_boards.md
+++ b/doc/user/group/epics/epic_boards.md
@@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
INFO:
Try epic boards and more with a
-[free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=p-epics-boards-docs).
+[free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=p-epics-boards-docs).
Epic boards build on the existing [epic tracking functionality](index.md) and
[labels](../../project/labels.md). Your epics appear as cards in vertical lists, organized by their assigned
diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md
index 998b8bf18ab..c7d793e0970 100644
--- a/doc/user/group/epics/index.md
+++ b/doc/user/group/epics/index.md
@@ -12,7 +12,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
INFO:
Check out [multi-level child epics](manage_epics.md#multi-level-child-epics) with a
-[free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=p-epics-docs).
+[free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=p-epics-docs).
When [issues](../../project/issues/index.md) share a theme across projects and milestones,
you can manage them by using epics.
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 0420fe12f89..989f25c2579 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -16,7 +16,7 @@ SAML on GitLab.com allows users to sign in through their SAML identity provider.
INFO:
Use your own SAML authentication to log in to [GitLab.com](http://gitlab.com/).
-[Try GitLab Ultimate free for 30 days](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=p-saml-sso-docs).
+[Try GitLab Ultimate free for 30 days](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=p-saml-sso-docs).
User synchronization of SAML SSO groups is supported through [SCIM](scim_setup.md). SCIM supports adding and removing users from the GitLab group automatically.
For example, if you remove a user from the SCIM app, SCIM removes that same user from the GitLab group.
diff --git a/doc/user/packages/go_proxy/index.md b/doc/user/packages/go_proxy/index.md
index 1cf3132489a..29455fdbb35 100644
--- a/doc/user/packages/go_proxy/index.md
+++ b/doc/user/packages/go_proxy/index.md
@@ -144,8 +144,8 @@ If you're unfamiliar with managing dependencies in Go, or Go in general, review
the following documentation:
- [Dependency Management in Go](../../../development/go_guide/dependencies.md)
-- [Go Modules Reference](https://golang.org/ref/mod)
-- [Documentation (`golang.org`)](https://golang.org/doc/)
+- [Go Modules Reference](https://go.dev/ref/mod)
+- [Documentation (`golang.org`)](https://go.dev/doc/)
- [Learn (`go.dev/learn`)](https://go.dev/learn/)
### Set environment variables
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index f6598f8846b..265a60c6f2c 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -539,7 +539,7 @@ server that has Python 3 installed, and may not work on other operating systems
or with other versions of Python.
1. Install Certbot by running the
- [`certbot-auto` wrapper script](https://certbot.eff.org/docs/install.html#certbot-auto).
+ [`certbot-auto` wrapper script](https://eff-certbot.readthedocs.io/install.html#certbot-auto).
On the command line of your server, run the following commands:
```shell
diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md
index 7cc8c314c63..a95e4d2bc26 100644
--- a/doc/user/project/code_owners.md
+++ b/doc/user/project/code_owners.md
@@ -13,7 +13,7 @@ type: reference
INFO:
Get access to Code Owners and more with a
-[free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=p-code-owners-docs).
+[free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=p-code-owners-docs).
Code Owners define who owns specific files or directories in a repository.
diff --git a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
index 978e35b3a9f..4e016bbc166 100644
--- a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
+++ b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
@@ -69,7 +69,7 @@ might be slightly different. Follow the
NOTE:
Read through CertBot's documentation on their
- [command line options](https://certbot.eff.org/docs/using.html#certbot-command-line-options).
+ [command line options](https://eff-certbot.readthedocs.io/using.html#certbot-command-line-options).
1. You're prompted with a message to agree with their terms.
Press `A` to agree and `Y` to let they log your IP.
diff --git a/doc/user/project/requirements/index.md b/doc/user/project/requirements/index.md
index 06f7dc1e375..294e493dfe9 100644
--- a/doc/user/project/requirements/index.md
+++ b/doc/user/project/requirements/index.md
@@ -19,7 +19,7 @@ stakeholders, system, software, or anything else you find important to capture.
INFO:
Meet your compliance needs with requirements in GitLab.
-[Try Ultimate free for 30 days](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=u-project-requirements-docs).
+[Try Ultimate free for 30 days](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=u-project-requirements-docs).
A requirement is an artifact in GitLab which describes the specific behavior of your product.
Requirements are long-lived and don't disappear unless manually cleared.
diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md
index 2bcfad97c25..b5b3f2d2085 100644
--- a/doc/user/project/working_with_projects.md
+++ b/doc/user/project/working_with_projects.md
@@ -331,7 +331,7 @@ Prerequisites:
To use a project as a Go package, use the `go get` and `godoc.org` discovery requests. You can use the meta tags:
-- [`go-import`](https://golang.org/cmd/go/#hdr-Remote_import_paths)
+- [`go-import`](https://pkg.go.dev/cmd/go#hdr-Remote_import_paths)
- [`go-source`](https://github.com/golang/gddo/wiki/Source-Code-Links)
### Authenticate Go requests to private projects
diff --git a/doc/user/search/advanced_search.md b/doc/user/search/advanced_search.md
index f0a3543f6af..b9c45bce43a 100644
--- a/doc/user/search/advanced_search.md
+++ b/doc/user/search/advanced_search.md
@@ -16,7 +16,7 @@ Advanced Search is enabled in GitLab.com.
INFO:
Get advanced search and more with a
-[free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial?glm_source=docs.gitlab.com&glm_content=p-advanced-search-docs).
+[free 30-day trial of GitLab Ultimate](https://about.gitlab.com/free-trial/index.html?glm_source=docs.gitlab.com&glm_content=p-advanced-search-docs).
GitLab Advanced Search expands on the Basic Search with an additional set of
features for faster, more advanced searches across the entire GitLab instance
diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb
index 25cc4e53bd2..f0c0182a02f 100644
--- a/lib/api/group_export.rb
+++ b/lib/api/group_export.rb
@@ -18,7 +18,7 @@ module API
detail 'This feature was introduced in GitLab 12.5.'
end
get ':id/export/download' do
- check_rate_limit! :group_download_export, [current_user, user_group]
+ check_rate_limit! :group_download_export, scope: [current_user, user_group]
if user_group.export_file_exists?
if user_group.export_archive_exists?
@@ -35,7 +35,7 @@ module API
detail 'This feature was introduced in GitLab 12.5.'
end
post ':id/export' do
- check_rate_limit! :group_export, [current_user]
+ check_rate_limit! :group_export, scope: current_user
export_service = ::Groups::ImportExport::ExportService.new(group: user_group, user: current_user)
diff --git a/lib/api/helpers/rate_limiter.rb b/lib/api/helpers/rate_limiter.rb
index 3a16aef6a74..7d87c74097d 100644
--- a/lib/api/helpers/rate_limiter.rb
+++ b/lib/api/helpers/rate_limiter.rb
@@ -2,26 +2,27 @@
module API
module Helpers
+ # == RateLimiter
+ #
+ # Helper that checks if the rate limit for a given endpoint is throttled by calling the
+ # Gitlab::ApplicationRateLimiter class. If the action is throttled for the current user, the request
+ # will be logged and an error message will be rendered with a Too Many Requests response status.
+ # See app/controllers/concerns/check_rate_limit.rb for Rails controllers version
module RateLimiter
- def check_rate_limit!(key, scope, users_allowlist = nil)
- if rate_limiter.throttled?(key, scope: scope, users_allowlist: users_allowlist)
- log_request(key)
- render_exceeded_limit_error!
- end
- end
+ def check_rate_limit!(key, scope:, **options)
+ return unless rate_limiter.throttled?(key, scope: scope, **options)
- private
+ rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
- def rate_limiter
- ::Gitlab::ApplicationRateLimiter
- end
+ return yield if block_given?
- def render_exceeded_limit_error!
render_api_error!({ error: _('This endpoint has been requested too many times. Try again later.') }, 429)
end
- def log_request(key)
- rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
+ private
+
+ def rate_limiter
+ ::Gitlab::ApplicationRateLimiter
end
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 0f91cc4a837..4d67cbd1272 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -262,7 +262,7 @@ module API
post ':id/issues' do
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/21140')
- check_rate_limit! :issues_create, [current_user] if Feature.disabled?("rate_limited_service_issues_create", user_project, default_enabled: :yaml)
+ check_rate_limit!(:issues_create, scope: current_user) if Feature.disabled?("rate_limited_service_issues_create", user_project, default_enabled: :yaml)
authorize! :create_issue, user_project
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 7629f84cec2..93ef77d5a62 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -79,7 +79,7 @@ module API
post ":id/#{noteables_str}/:noteable_id/notes", feature_category: feature_category do
allowlist =
Gitlab::CurrentSettings.current_application_settings.notes_create_limit_allowlist
- check_rate_limit! :notes_create, [current_user], allowlist
+ check_rate_limit! :notes_create, scope: current_user, users_allowlist: allowlist
noteable = find_noteable(noteable_type, params[:noteable_id])
opts = {
diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb
index e01c195dbc4..843f72c0e1d 100644
--- a/lib/api/project_export.rb
+++ b/lib/api/project_export.rb
@@ -25,7 +25,7 @@ module API
detail 'This feature was introduced in GitLab 10.6.'
end
get ':id/export/download' do
- check_rate_limit! :project_download_export, [current_user, user_project]
+ check_rate_limit! :project_download_export, scope: [current_user, user_project]
if user_project.export_file_exists?
if user_project.export_archive_exists?
@@ -49,7 +49,7 @@ module API
end
end
post ':id/export' do
- check_rate_limit! :project_export, [current_user]
+ check_rate_limit! :project_export, scope: current_user
user_project.remove_exports
diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb
index 56b4f4c6598..7bdcaa5a26f 100644
--- a/lib/api/project_import.rb
+++ b/lib/api/project_import.rb
@@ -81,7 +81,7 @@ module API
post 'import' do
require_gitlab_workhorse!
- check_rate_limit! :project_import, [current_user, :project_import]
+ check_rate_limit! :project_import, scope: [current_user, :project_import]
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/21041')
@@ -135,7 +135,7 @@ module API
post 'remote-import' do
not_found! unless ::Feature.enabled?(:import_project_from_remote_file)
- check_rate_limit! :project_import, [current_user, :project_import]
+ check_rate_limit! :project_import, scope: [current_user, :project_import]
response = ::Import::GitlabProjects::CreateProjectFromRemoteFileService.new(
current_user,
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 17b65fa21c3..fc976c23726 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -45,7 +45,7 @@ module API
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do
- include ::Gitlab::RateLimitHelpers
+ include Gitlab::RepositoryArchiveRateLimiter
def handle_project_member_errors(errors)
if errors[:project_access].any?
@@ -150,8 +150,8 @@ module API
optional :path, type: String, desc: 'Subfolder of the repository to be downloaded'
end
get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
- if archive_rate_limit_reached?(current_user, user_project)
- render_api_error!({ error: ::Gitlab::RateLimitHelpers::ARCHIVE_RATE_LIMIT_REACHED_MESSAGE }, 429)
+ check_archive_rate_limit!(current_user, user_project) do
+ render_api_error!({ error: _('This archive has been requested too many times. Try again later.') }, 429)
end
not_acceptable! if Gitlab::HotlinkingDetector.intercept_hotlinking?(request)
diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb
index 677d0840208..d6c026963e1 100644
--- a/lib/api/v3/github.rb
+++ b/lib/api/v3/github.rb
@@ -101,8 +101,6 @@ module API
# of time after a Gitaly timeout, to mitigate frequent Gitaly timeouts
# for some Commit diffs.
def diff_files(commit)
- return commit.diffs.diff_files unless Feature.enabled?(:api_v3_commits_skip_diff_files, commit.project, default_enabled: :yaml)
-
cache_key = [
GITALY_TIMEOUT_CACHE_KEY,
commit.project.id,
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index 4694c87183a..fb90ad9e275 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -4,12 +4,7 @@ module Gitlab
# This class implements a simple rate limiter that can be used to throttle
# certain actions. Unlike Rack Attack and Rack::Throttle, which operate at
# the middleware level, this can be used at the controller or API level.
- #
- # @example
- # if Gitlab::ApplicationRateLimiter.throttled?(:project_export, scope: [@project, @current_user])
- # flash[:alert] = 'error!'
- # redirect_to(edit_project_path(@project), status: :too_many_requests)
- # end
+ # See CheckRateLimit concern for usage.
class ApplicationRateLimiter
InvalidKeyError = Class.new(StandardError)
@@ -47,7 +42,7 @@ module Gitlab
project_import: { threshold: -> { application_settings.project_import_limit }, interval: 1.minute },
project_testing_hook: { threshold: 5, interval: 1.minute },
play_pipeline_schedule: { threshold: 1, interval: 1.minute },
- show_raw_controller: { threshold: -> { application_settings.raw_blob_request_limit }, interval: 1.minute },
+ raw_blob: { threshold: -> { application_settings.raw_blob_request_limit }, interval: 1.minute },
group_export: { threshold: -> { application_settings.group_export_limit }, interval: 1.minute },
group_download_export: { threshold: -> { application_settings.group_download_export_limit }, interval: 1.minute },
group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute },
diff --git a/lib/gitlab/ci/badge/metadata.rb b/lib/gitlab/ci/badge/metadata.rb
index eec9fedfaa9..244e3aff851 100644
--- a/lib/gitlab/ci/badge/metadata.rb
+++ b/lib/gitlab/ci/badge/metadata.rb
@@ -8,14 +8,13 @@ module Gitlab::Ci
class Metadata
include Gitlab::Routing
include ActionView::Helpers::AssetTagHelper
- include ActionView::Helpers::UrlHelper
def initialize(badge)
@badge = badge
end
def to_html
- link_to(image_tag(image_url, alt: title), link_url)
+ ApplicationController.helpers.link_to(image_tag(image_url, alt: title), link_url)
end
def to_markdown
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index acb1533f935..ddad56061a4 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -320,6 +320,7 @@ namespace_package_settings: :gitlab_main
namespace_root_storage_statistics: :gitlab_main
namespace_settings: :gitlab_main
namespaces: :gitlab_main
+namespaces_sync_events: :gitlab_main
namespace_statistics: :gitlab_main
note_diff_files: :gitlab_main
notes: :gitlab_main
@@ -414,6 +415,7 @@ project_repository_storage_moves: :gitlab_main
project_security_settings: :gitlab_main
project_settings: :gitlab_main
projects: :gitlab_main
+projects_sync_events: :gitlab_main
project_statistics: :gitlab_main
project_topics: :gitlab_main
project_tracing_settings: :gitlab_main
diff --git a/lib/gitlab/database/schema_helpers.rb b/lib/gitlab/database/schema_helpers.rb
index 3d929c62933..9ddc5391689 100644
--- a/lib/gitlab/database/schema_helpers.rb
+++ b/lib/gitlab/database/schema_helpers.rb
@@ -25,6 +25,7 @@ module Gitlab
CREATE TRIGGER #{name}
#{fires} ON #{table_name}
FOR EACH ROW
+ #{yield if block_given?}
EXECUTE FUNCTION #{function_name}()
SQL
end
diff --git a/lib/gitlab/database/type/json_pg_safe.rb b/lib/gitlab/database/type/json_pg_safe.rb
new file mode 100644
index 00000000000..bbc207bd0d9
--- /dev/null
+++ b/lib/gitlab/database/type/json_pg_safe.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Type
+ # Extends Rails' ActiveRecord::Type::Json data type to remove JSON
+ # encooded nullbytes `\u0000` to prevent PostgreSQL errors like
+ # `PG::UntranslatableCharacter: ERROR: unsupported Unicode escape
+ # sequence`.
+ #
+ # Example:
+ #
+ # class SomeModel < ApplicationRecord
+ # # some_model.a_field is of type `jsonb`
+ # attribute :a_field, Gitlab::Database::Type::JsonPgSafe.new
+ # end
+ class JsonPgSafe < ActiveRecord::Type::Json
+ def serialize(value)
+ super&.gsub('\u0000', '')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/rate_limit_helpers.rb b/lib/gitlab/rate_limit_helpers.rb
deleted file mode 100644
index 653410a40a5..00000000000
--- a/lib/gitlab/rate_limit_helpers.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module RateLimitHelpers
- ARCHIVE_RATE_LIMIT_REACHED_MESSAGE = 'This archive has been requested too many times. Try again later.'
- ARCHIVE_RATE_ANONYMOUS_THRESHOLD = 100 # Allow 100 requests/min for anonymous users
- ARCHIVE_RATE_THROTTLE_KEY = :project_repositories_archive
-
- def archive_rate_limit_reached?(user, project)
- return false unless Feature.enabled?(:archive_rate_limit)
-
- key = ARCHIVE_RATE_THROTTLE_KEY
-
- if rate_limiter.throttled?(key, scope: [project, user], threshold: archive_rate_threshold_by_user(user))
- rate_limiter.log_request(request, "#{key}_request_limit".to_sym, user)
-
- return true
- end
-
- false
- end
-
- def archive_rate_threshold_by_user(user)
- if user
- nil # Use the defaults
- else
- ARCHIVE_RATE_ANONYMOUS_THRESHOLD
- end
- end
-
- def rate_limiter
- ::Gitlab::ApplicationRateLimiter
- end
- end
-end
diff --git a/lib/gitlab/repository_archive_rate_limiter.rb b/lib/gitlab/repository_archive_rate_limiter.rb
new file mode 100644
index 00000000000..31a3dc34bf6
--- /dev/null
+++ b/lib/gitlab/repository_archive_rate_limiter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module RepositoryArchiveRateLimiter
+ def check_archive_rate_limit!(current_user, project, &block)
+ return unless Feature.enabled?(:archive_rate_limit)
+
+ threshold = current_user ? nil : 100
+
+ check_rate_limit!(:project_repositories_archive, scope: [project, current_user], threshold: threshold, &block)
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b0ff3ba8175..30b4c4ddf29 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8813,6 +8813,15 @@ msgstr ""
msgid "ComplianceFramework|New compliance framework"
msgstr ""
+msgid "ComplianceReport|Approved by author"
+msgstr ""
+
+msgid "ComplianceReport|Approved by committer"
+msgstr ""
+
+msgid "ComplianceReport|Less than 2 approvers"
+msgstr ""
+
msgid "Component"
msgstr ""
@@ -35576,9 +35585,6 @@ msgstr ""
msgid "This action cannot be undone, and will permanently delete the %{key} SSH key"
msgstr ""
-msgid "This action has been performed too many times. Try again later."
-msgstr ""
-
msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all related resources, including issues and merge requests."
msgstr ""
@@ -35603,6 +35609,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This archive has been requested too many times. Try again later."
+msgstr ""
+
msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{count} %{issuables} have been included. Consider re-exporting with a narrower selection of %{issuables}."
msgstr ""
diff --git a/spec/controllers/profiles/emails_controller_spec.rb b/spec/controllers/profiles/emails_controller_spec.rb
index ce16632472f..214a893f0fa 100644
--- a/spec/controllers/profiles/emails_controller_spec.rb
+++ b/spec/controllers/profiles/emails_controller_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Profiles::EmailsController do
subject
expect(response).to have_gitlab_http_status(:redirect)
- expect(flash[:alert]).to eq(_('This action has been performed too many times. Try again later.'))
+ expect(flash[:alert]).to eq(_('This endpoint has been requested too many times. Try again later.'))
end
end
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index cb2579b800a..b7eef3812a4 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe Projects::RepositoriesController do
describe 'rate limiting' do
it 'rate limits user when thresholds hit' do
- expect(controller).to receive(:archive_rate_limit_reached?).and_return(true)
+ allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master' }, format: "html"
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index dc7066f6b61..d50f1aa1dd8 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -25,6 +25,17 @@ RSpec.describe Projects::Settings::CiCdController do
expect(response).to render_template(:show)
end
+ context 'with CI/CD disabled' do
+ before do
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ end
+
+ it 'renders show with 404 status code' do
+ get :show, params: { namespace_id: project.namespace, project_id: project }
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'with group runners' do
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [other_project]) }
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 544cce00dba..cc2d36221dc 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -59,6 +59,19 @@ RSpec.describe "Admin Runners" do
end
end
+ it 'shows a job count' do
+ runner = create(:ci_runner, :project, projects: [project])
+
+ create(:ci_build, runner: runner)
+ create(:ci_build, runner: runner)
+
+ visit admin_runners_path
+
+ within "[data-testid='runner-row-#{runner.id}'] [data-label='Jobs']" do
+ expect(page).to have_content '2'
+ end
+ end
+
describe 'delete runner' do
let!(:runner) { create(:ci_runner, description: 'runner-foo') }
diff --git a/spec/fixtures/error_tracking/parsed_event_nullbytes.json b/spec/fixtures/error_tracking/parsed_event_nullbytes.json
new file mode 100644
index 00000000000..570a5a329a4
--- /dev/null
+++ b/spec/fixtures/error_tracking/parsed_event_nullbytes.json
@@ -0,0 +1,175 @@
+{
+ "breadcrumbs" : {
+ "values" : [
+ {
+ "category" : "start_processing.action_controller",
+ "data" : {
+ "action" : "error2",
+ "controller" : "PostsController",
+ "format" : "html",
+ "method" : "GET",
+ "params" : {
+ "action" : "error2",
+ "controller" : "posts"
+ },
+ "path" : "/posts/error2",
+ "start_timestamp" : 1625749156.5553
+ },
+ "level" : null,
+ "message" : "",
+ "timestamp" : 1625749156,
+ "type" : null
+ },
+ {
+ "category" : "process_action.action_controller",
+ "data" : {
+ "action" : "error2",
+ "controller" : "PostsController",
+ "db_runtime" : 0,
+ "format" : "html",
+ "method" : "GET",
+ "params" : {
+ "action" : "error2",
+ "controller" : "posts"
+ },
+ "path" : "/posts/error2",
+ "start_timestamp" : 1625749156.55539,
+ "view_runtime" : null
+ },
+ "level" : null,
+ "message" : "",
+ "timestamp" : 1625749156,
+ "type" : null
+ }
+ ]
+ },
+ "contexts" : {
+ "os" : {
+ "build" : "20.5.0",
+ "kernel_version" : "Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64",
+ "name" : "Darwin",
+ "version" : "Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64"
+ },
+ "runtime" : {
+ "name" : "ruby",
+ "version" : "ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin19]"
+ },
+ "trace" : {
+ "description" : null,
+ "op" : "rails.request",
+ "parent_span_id" : null,
+ "span_id" : "4a3ed8701e7f4ea4",
+ "status" : null,
+ "trace_id" : "d82b93fbc39e4d13b85762afa2e3ff36"
+ }
+ },
+ "environment" : "development",
+ "event_id" : "7c9ae6e58f03442b9203bbdcf6ae904c",
+ "exception" : {
+ "values" : [
+ {
+ "module" : "ActionView",
+ "stacktrace" : {
+ "frames" : [
+ {
+ "abs_path" : "/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/puma-3.12.6/lib/puma/thread_pool.rb",
+ "context_line" : " block.call(work, *extra)\n",
+ "filename" : "puma/thread_pool.rb",
+ "function" : "block in spawn_thread",
+ "in_app" : false,
+ "lineno" : 135,
+ "post_context" : [
+ " rescue Exception => e\u0000\n",
+ " STDERR.puts \"Error\u0000reached top of thread-pool: #{e.message} (#{e.class})\"\n",
+ " end\n"
+ ],
+ "pre_context" : [
+ " end\n",
+ "\n",
+ " begin\n"
+ ],
+ "project_root" : "/Users/developer/rails-project"
+ },
+ {
+ "abs_path" : "/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/puma-3.12.6/lib/puma/server.rb",
+ "context_line" : " process_client client, buffer\n",
+ "filename" : "puma/server.rb",
+ "function" : "block in run",
+ "in_app" : false,
+ "lineno" : 334,
+ "post_context" : [
+ " else\n",
+ " client.set_timeout @first_data_timeout\n",
+ " @reactor.add client\n"
+ ],
+ "pre_context" : [
+ " client.close\n",
+ " else\n",
+ " if process_now\n"
+ ],
+ "project_root" : "/Users/developer/rails-project"
+ },
+ {
+ "abs_path" : "/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.6/lib/action_view/path_set.rb",
+ "context_line" : " find_all(*args).first || raise(MissingTemplate.new(self, *args))\n",
+ "filename" : "action_view/path_set.rb",
+ "function" : "find",
+ "in_app" : false,
+ "lineno" : 48,
+ "post_context" : [
+ " end\n",
+ "\n",
+ " def find_file(path, prefixes = [], *args)\n"
+ ],
+ "pre_context" : [
+ " end\n",
+ "\n",
+ " def find(*args)\n"
+ ],
+ "project_root" : "/Users/developer/rails-project"
+ }
+ ]
+ },
+ "thread_id" : 70254489510160,
+ "type" : "ActionView::MissingTemplate",
+ "value" : "Missing template posts/error2, application/error2 with {:locale=>[:en], :formats=>[:html], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]}. Searched in:\n * \"/Users/developer/rails-project/app/views\"\n"
+ }
+ ]
+ },
+ "extra" : {},
+ "fingerprint" : [],
+ "level" : "error",
+ "message" : "",
+ "modules" : {
+ "concurrent-ruby" : "1.1.9",
+ "i18n" : "1.8.10",
+ "minitest" : "5.14.4",
+ "rake" : "13.0.3",
+ "thread_safe" : "0.3.6",
+ "tzinfo" : "1.2.9",
+ "uglifier" : "4.2.0",
+ "web-console" : "3.7.0"
+ },
+ "platform" : "ruby",
+ "release" : "db853d7",
+ "request" : {
+ "env" : {
+ "SERVER_NAME" : "localhost",
+ "SERVER_PORT" : "4444"
+ },
+ "headers" : {},
+ "method" : "GET",
+ "url" : "http://localhost/posts/error2"
+ },
+ "sdk" : {
+ "name" : "sentry.ruby.rails",
+ "version" : "4.5.1"
+ },
+ "server_name" : "MacBook.local",
+ "tags" : {
+ "request_id" : "4253dcd9-5e48-474a-89b4-0e945ab825af"
+ },
+ "timestamp" : "2021-07-08T12:59:16Z",
+ "transaction" : "PostsController#error2",
+ "user" : {}
+}
diff --git a/spec/frontend/issues/type_selector/components/__snapshots__/info_popover_spec.js.snap b/spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap
index 196fbb8a643..881dcda126f 100644
--- a/spec/frontend/issues/type_selector/components/__snapshots__/info_popover_spec.js.snap
+++ b/spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap
@@ -1,12 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Issuable type info popover renders 1`] = `
+exports[`Issue type info popover renders 1`] = `
<span
id="popovercontainer"
>
<gl-icon-stub
class="gl-ml-5 gl-text-gray-500"
- id="issuable-type-info"
+ id="issue-type-info"
name="question-o"
size="16"
/>
@@ -14,7 +14,7 @@ exports[`Issuable type info popover renders 1`] = `
<gl-popover-stub
container="popovercontainer"
cssclasses=""
- target="issuable-type-info"
+ target="issue-type-info"
title="Issue types"
triggers="focus hover"
>
diff --git a/spec/frontend/issues/suggestions/components/item_spec.js b/spec/frontend/issues/new/components/title_suggestions_item_spec.js
index a41ba7a38c2..5eb30b52de5 100644
--- a/spec/frontend/issues/suggestions/components/item_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_item_spec.js
@@ -1,15 +1,15 @@
import { GlTooltip, GlLink, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import Suggestion from '~/issues/suggestions/components/item.vue';
+import TitleSuggestionsItem from '~/issues/new/components/title_suggestions_item.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import mockData from '../mock_data';
-describe('Issuable suggestions suggestion component', () => {
+describe('Issue title suggestions item component', () => {
let wrapper;
function createComponent(suggestion = {}) {
- wrapper = shallowMount(Suggestion, {
+ wrapper = shallowMount(TitleSuggestionsItem, {
propsData: {
suggestion: {
...mockData(),
diff --git a/spec/frontend/issues/suggestions/components/app_spec.js b/spec/frontend/issues/new/components/title_suggestions_spec.js
index e47a1a6da82..984d0c9d25b 100644
--- a/spec/frontend/issues/suggestions/components/app_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_spec.js
@@ -1,12 +1,12 @@
import { shallowMount } from '@vue/test-utils';
-import App from '~/issues/suggestions/components/app.vue';
-import Suggestion from '~/issues/suggestions/components/item.vue';
+import TitleSuggestions from '~/issues/new/components/title_suggestions.vue';
+import TitleSuggestionsItem from '~/issues/new/components/title_suggestions_item.vue';
-describe('Issuable suggestions app component', () => {
+describe('Issue title suggestions component', () => {
let wrapper;
function createComponent(search = 'search') {
- wrapper = shallowMount(App, {
+ wrapper = shallowMount(TitleSuggestions, {
propsData: {
search,
projectPath: 'project',
@@ -77,7 +77,7 @@ describe('Issuable suggestions app component', () => {
wrapper.setData(data);
return wrapper.vm.$nextTick(() => {
- expect(wrapper.findAll(Suggestion).length).toBe(2);
+ expect(wrapper.findAll(TitleSuggestionsItem).length).toBe(2);
});
});
diff --git a/spec/frontend/issues/type_selector/components/info_popover_spec.js b/spec/frontend/issues/new/components/type_popover_spec.js
index b4844114018..fe3d5207516 100644
--- a/spec/frontend/issues/type_selector/components/info_popover_spec.js
+++ b/spec/frontend/issues/new/components/type_popover_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import InfoPopover from '~/issues/type_selector/components/info_popover.vue';
+import TypePopover from '~/issues/new/components/type_popover.vue';
-describe('Issuable type info popover', () => {
+describe('Issue type info popover', () => {
let wrapper;
function createComponent() {
- wrapper = shallowMount(InfoPopover);
+ wrapper = shallowMount(TypePopover);
}
afterEach(() => {
diff --git a/spec/frontend/issues/suggestions/mock_data.js b/spec/frontend/issues/new/mock_data.js
index 74b569d9833..74b569d9833 100644
--- a/spec/frontend/issues/suggestions/mock_data.js
+++ b/spec/frontend/issues/new/mock_data.js
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index de1be5bc337..3e2ba918d9b 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1040,4 +1040,15 @@ describe('common_utils', () => {
expect(result).toEqual(['hello', 'helloWorld']);
});
});
+
+ describe('convertArrayOfObjectsToCamelCase', () => {
+ it('returns a new array with snake_case object property names converted camelCase', () => {
+ const result = commonUtils.convertArrayOfObjectsToCamelCase([
+ { hello: '' },
+ { hello_world: '' },
+ ]);
+
+ expect(result).toEqual([{ hello: '' }, { helloWorld: '' }]);
+ });
+ });
});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index e8e2b979685..5a14fa5a2d5 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -46,6 +46,7 @@ describe('RunnerList', () => {
'Runner ID',
'Version',
'IP Address',
+ 'Jobs',
'Tags',
'Last contact',
'', // actions has no label
@@ -79,6 +80,7 @@ describe('RunnerList', () => {
// Other fields
expect(findCell({ fieldKey: 'version' }).text()).toBe(version);
expect(findCell({ fieldKey: 'ipAddress' }).text()).toBe(ipAddress);
+ expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0');
expect(findCell({ fieldKey: 'tagList' }).text()).toBe('');
expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String));
@@ -89,6 +91,42 @@ describe('RunnerList', () => {
expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
});
+ describe('Table data formatting', () => {
+ let mockRunnersCopy;
+
+ beforeEach(() => {
+ mockRunnersCopy = [
+ {
+ ...mockRunners[0],
+ },
+ ];
+ });
+
+ it('Formats job counts', () => {
+ mockRunnersCopy[0].jobCount = 1;
+
+ createComponent({ props: { runners: mockRunnersCopy } }, mount);
+
+ expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1');
+ });
+
+ it('Formats large job counts', () => {
+ mockRunnersCopy[0].jobCount = 1000;
+
+ createComponent({ props: { runners: mockRunnersCopy } }, mount);
+
+ expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000');
+ });
+
+ it('Formats large job counts with a plus symbol', () => {
+ mockRunnersCopy[0].jobCount = 1001;
+
+ createComponent({ props: { runners: mockRunnersCopy } }, mount);
+
+ expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000+');
+ });
+ });
+
it('Shows runner identifier', () => {
const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
diff --git a/spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb
index f1dbfbbff18..25fc676d09e 100644
--- a/spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/loose_foreign_key_helpers_spec.rb
@@ -47,11 +47,16 @@ RSpec.describe Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers do
record_to_be_deleted.delete
expect(LooseForeignKeys::DeletedRecord.count).to eq(1)
- deleted_record = LooseForeignKeys::DeletedRecord.all.first
+
+ arel_table = LooseForeignKeys::DeletedRecord.arel_table
+ deleted_record = LooseForeignKeys::DeletedRecord
+ .select(arel_table[Arel.star], arel_table[:partition].as('partition_number')) # aliasing the ignored partition column to partition_number
+ .all
+ .first
expect(deleted_record.primary_key_value).to eq(record_to_be_deleted.id)
expect(deleted_record.fully_qualified_table_name).to eq('public._test_loose_fk_test_table')
- expect(deleted_record.partition).to eq(1)
+ expect(deleted_record.partition_number).to eq(1)
end
it 'stores multiple record deletions' do
diff --git a/spec/lib/gitlab/database/type/json_pg_safe_spec.rb b/spec/lib/gitlab/database/type/json_pg_safe_spec.rb
new file mode 100644
index 00000000000..91dc6f39aa7
--- /dev/null
+++ b/spec/lib/gitlab/database/type/json_pg_safe_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Type::JsonPgSafe do
+ let(:type) { described_class.new }
+
+ describe '#serialize' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { type.serialize(value) }
+
+ where(:value, :json) do
+ nil | nil
+ 1 | '1'
+ 1.0 | '1.0'
+ "str\0ing\u0000" | '"string"'
+ ["\0arr", "a\u0000y"] | '["arr","ay"]'
+ { "key\0" => "value\u0000\0" } | '{"key":"value"}'
+ end
+
+ with_them do
+ it { is_expected.to eq(json) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 49052623436..cabc7cff8a1 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -597,6 +597,8 @@ project:
- security_scans
- ci_feature_usages
- bulk_import_exports
+- ci_project_mirror
+- sync_events
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/rate_limit_helpers_spec.rb b/spec/lib/gitlab/rate_limit_helpers_spec.rb
deleted file mode 100644
index ad0e2de1448..00000000000
--- a/spec/lib/gitlab/rate_limit_helpers_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::RateLimitHelpers, :clean_gitlab_redis_rate_limiting do
- let(:limiter_class) do
- Class.new do
- include ::Gitlab::RateLimitHelpers
-
- attr_reader :request
-
- def initialize(request)
- @request = request
- end
- end
- end
-
- let(:request) { instance_double(ActionDispatch::Request, request_method: 'GET', ip: '127.0.0.1', fullpath: '/') }
- let(:class_instance) { limiter_class.new(request) }
-
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
-
- describe '#archive_rate_limit_reached?' do
- context 'with a user' do
- it 'rate limits the user properly' do
- 5.times do
- expect(class_instance.archive_rate_limit_reached?(user, project)).to be_falsey
- end
-
- expect(class_instance.archive_rate_limit_reached?(user, project)).to be_truthy
- end
- end
-
- context 'with an anonymous user' do
- before do
- stub_const('Gitlab::RateLimitHelpers::ARCHIVE_RATE_ANONYMOUS_THRESHOLD', 2)
- end
-
- it 'rate limits with higher limits' do
- 2.times do
- expect(class_instance.archive_rate_limit_reached?(nil, project)).to be_falsey
- end
-
- expect(class_instance.archive_rate_limit_reached?(nil, project)).to be_truthy
- expect(class_instance.archive_rate_limit_reached?(user, project)).to be_falsey
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/repository_archive_rate_limiter_spec.rb b/spec/lib/gitlab/repository_archive_rate_limiter_spec.rb
new file mode 100644
index 00000000000..49df70f3cb3
--- /dev/null
+++ b/spec/lib/gitlab/repository_archive_rate_limiter_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::RepositoryArchiveRateLimiter do
+ let(:described_class) do
+ Class.new do
+ include ::Gitlab::RepositoryArchiveRateLimiter
+
+ def check_rate_limit!(**args)
+ end
+ end
+ end
+
+ describe "#check_archive_rate_limit!" do
+ let(:project) { instance_double('Project') }
+ let(:current_user) { instance_double('User') }
+ let(:check) { subject.check_archive_rate_limit!(current_user, project) }
+
+ context 'when archive_rate_limit feature flag is disabled' do
+ before do
+ stub_feature_flags(archive_rate_limit: false)
+ end
+
+ it 'does not check rate limit' do
+ expect(subject).not_to receive(:check_rate_limit!)
+
+ expect(check).to eq nil
+ end
+ end
+
+ context 'when archive_rate_limit feature flag is enabled' do
+ before do
+ stub_feature_flags(archive_rate_limit: true)
+ end
+
+ context 'when current user exists' do
+ it 'checks for project_repositories_archive rate limiting with default threshold' do
+ expect(subject).to receive(:check_rate_limit!)
+ .with(:project_repositories_archive, scope: [project, current_user], threshold: nil)
+ check
+ end
+ end
+
+ context 'when current user does not exist' do
+ let(:current_user) { nil }
+
+ it 'checks for project_repositories_archive rate limiting with threshold 100' do
+ expect(subject).to receive(:check_rate_limit!)
+ .with(:project_repositories_archive, scope: [project, current_user], threshold: 100)
+ check
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/namespace_mirror_spec.rb b/spec/models/ci/namespace_mirror_spec.rb
new file mode 100644
index 00000000000..b4c71f51377
--- /dev/null
+++ b/spec/models/ci/namespace_mirror_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::NamespaceMirror do
+ let!(:group1) { create(:group) }
+ let!(:group2) { create(:group, parent: group1) }
+ let!(:group3) { create(:group, parent: group2) }
+ let!(:group4) { create(:group, parent: group3) }
+
+ describe '.sync!' do
+ let!(:event) { namespace.sync_events.create! }
+
+ subject(:sync) { described_class.sync!(event.reload) }
+
+ context 'when namespace hierarchy does not exist in the first place' do
+ let(:namespace) { group3 }
+
+ it 'creates the hierarchy' do
+ expect { sync }.to change { described_class.count }.from(0).to(1)
+
+ expect(namespace.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
+ end
+ end
+
+ context 'when namespace hierarchy does already exist' do
+ let(:namespace) { group3 }
+
+ before do
+ described_class.create!(namespace: namespace, traversal_ids: [namespace.id])
+ end
+
+ it 'updates the hierarchy' do
+ expect { sync }.not_to change { described_class.count }
+
+ expect(namespace.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
+ end
+ end
+
+ # I did not extract this context to a `shared_context` because the behavior will change
+ # after implementing the TODO in `Ci::NamespaceMirror.sync!`
+ context 'changing the middle namespace' do
+ let(:namespace) { group2 }
+
+ before do
+ described_class.create!(namespace_id: group1.id, traversal_ids: [group1.id])
+ described_class.create!(namespace_id: group2.id, traversal_ids: [group1.id, group2.id])
+ described_class.create!(namespace_id: group3.id, traversal_ids: [group1.id, group2.id, group3.id])
+ described_class.create!(namespace_id: group4.id, traversal_ids: [group1.id, group2.id, group3.id, group4.id])
+
+ group2.update!(parent: nil)
+ end
+
+ it 'updates hierarchies for the base but wait for events for the children' do
+ expect { sync }.not_to change { described_class.count }
+
+ expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id])
+ expect(group2.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id])
+ expect(group3.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id])
+ expect(group4.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id, group4.id])
+ end
+ end
+
+ context 'when the FFs sync_traversal_ids, use_traversal_ids and use_traversal_ids_for_ancestors are disabled' do
+ before do
+ stub_feature_flags(sync_traversal_ids: false,
+ use_traversal_ids: false,
+ use_traversal_ids_for_ancestors: false)
+ end
+
+ context 'changing the middle namespace' do
+ let(:namespace) { group2 }
+
+ before do
+ described_class.create!(namespace_id: group1.id, traversal_ids: [group1.id])
+ described_class.create!(namespace_id: group2.id, traversal_ids: [group1.id, group2.id])
+ described_class.create!(namespace_id: group3.id, traversal_ids: [group1.id, group2.id, group3.id])
+ described_class.create!(namespace_id: group4.id, traversal_ids: [group1.id, group2.id, group3.id, group4.id])
+
+ group2.update!(parent: nil)
+ end
+
+ it 'updates hierarchies for the base and descendants' do
+ expect { sync }.not_to change { described_class.count }
+
+ expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id])
+ expect(group2.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id])
+ expect(group3.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id])
+ expect(group4.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id, group4.id])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/project_mirror_spec.rb b/spec/models/ci/project_mirror_spec.rb
new file mode 100644
index 00000000000..199285b036c
--- /dev/null
+++ b/spec/models/ci/project_mirror_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::ProjectMirror do
+ let_it_be(:group1) { create(:group) }
+ let_it_be(:group2) { create(:group) }
+
+ let!(:project) { create(:project, namespace: group2) }
+
+ describe '.sync!' do
+ let!(:event) { Projects::SyncEvent.create!(project: project) }
+
+ subject(:sync) { described_class.sync!(event.reload) }
+
+ context 'when project hierarchy does not exist in the first place' do
+ it 'creates a ci_projects record' do
+ expect { sync }.to change { described_class.count }.from(0).to(1)
+
+ expect(project.ci_project_mirror).to have_attributes(namespace_id: group2.id)
+ end
+ end
+
+ context 'when project hierarchy does already exist' do
+ before do
+ described_class.create!(project_id: project.id, namespace_id: group1.id)
+ end
+
+ it 'updates the related ci_projects record' do
+ expect { sync }.not_to change { described_class.count }
+
+ expect(project.ci_project_mirror).to have_attributes(namespace_id: group2.id)
+ end
+ end
+ end
+end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 846dfb30928..51fdbfebd3a 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -223,9 +223,9 @@ RSpec.describe ContainerRepository do
end
end
- describe '.create_from_path!' do
+ describe '.find_or_create_from_path' do
let(:repository) do
- described_class.create_from_path!(ContainerRegistry::Path.new(path))
+ described_class.find_or_create_from_path(ContainerRegistry::Path.new(path))
end
let(:repository_path) { ContainerRegistry::Path.new(path) }
@@ -291,6 +291,35 @@ RSpec.describe ContainerRepository do
expect(repository.id).to eq(container_repository.id)
end
end
+
+ context 'when many of the same repository are created at the same time' do
+ let(:path) { ContainerRegistry::Path.new(project.full_path + '/some/image') }
+
+ it 'does not throw validation errors and only creates one repository' do
+ expect { repository_creation_race(path) }.to change { ContainerRepository.count }.by(1)
+ end
+
+ it 'retrieves a persisted repository for all concurrent calls' do
+ repositories = repository_creation_race(path).map(&:value)
+
+ expect(repositories).to all(be_persisted)
+ end
+ end
+
+ def repository_creation_race(path)
+ # create a race condition - structure from https://blog.arkency.com/2015/09/testing-race-conditions/
+ wait_for_it = true
+
+ threads = Array.new(10) do |i|
+ Thread.new do
+ true while wait_for_it
+
+ ::ContainerRepository.find_or_create_from_path(path)
+ end
+ end
+ wait_for_it = false
+ threads.each(&:join)
+ end
end
describe '.build_root_repository' do
diff --git a/spec/models/loose_foreign_keys/deleted_record_spec.rb b/spec/models/loose_foreign_keys/deleted_record_spec.rb
index cd5068bdb52..07ffff746a5 100644
--- a/spec/models/loose_foreign_keys/deleted_record_spec.rb
+++ b/spec/models/loose_foreign_keys/deleted_record_spec.rb
@@ -5,31 +5,148 @@ require 'spec_helper'
RSpec.describe LooseForeignKeys::DeletedRecord, type: :model do
let_it_be(:table) { 'public.projects' }
- let_it_be(:deleted_record_1) { described_class.create!(partition: 1, fully_qualified_table_name: table, primary_key_value: 5) }
- let_it_be(:deleted_record_2) { described_class.create!(partition: 1, fully_qualified_table_name: table, primary_key_value: 1) }
- let_it_be(:deleted_record_3) { described_class.create!(partition: 1, fully_qualified_table_name: 'public.other_table', primary_key_value: 3) }
- let_it_be(:deleted_record_4) { described_class.create!(partition: 1, fully_qualified_table_name: table, primary_key_value: 1) } # duplicate
+ describe 'class methods' do
+ let_it_be(:deleted_record_1) { described_class.create!(fully_qualified_table_name: table, primary_key_value: 5) }
+ let_it_be(:deleted_record_2) { described_class.create!(fully_qualified_table_name: table, primary_key_value: 1) }
+ let_it_be(:deleted_record_3) { described_class.create!(fully_qualified_table_name: 'public.other_table', primary_key_value: 3) }
+ let_it_be(:deleted_record_4) { described_class.create!(fully_qualified_table_name: table, primary_key_value: 1) } # duplicate
- describe '.load_batch_for_table' do
- it 'loads records and orders them by creation date' do
- records = described_class.load_batch_for_table(table, 10)
+ describe '.load_batch_for_table' do
+ it 'loads records and orders them by creation date' do
+ records = described_class.load_batch_for_table(table, 10)
- expect(records).to eq([deleted_record_1, deleted_record_2, deleted_record_4])
+ expect(records).to eq([deleted_record_1, deleted_record_2, deleted_record_4])
+ end
+
+ it 'supports configurable batch size' do
+ records = described_class.load_batch_for_table(table, 2)
+
+ expect(records).to eq([deleted_record_1, deleted_record_2])
+ end
end
- it 'supports configurable batch size' do
- records = described_class.load_batch_for_table(table, 2)
+ describe '.mark_records_processed' do
+ it 'updates all records' do
+ records = described_class.load_batch_for_table(table, 10)
+ described_class.mark_records_processed(records)
- expect(records).to eq([deleted_record_1, deleted_record_2])
+ expect(described_class.status_pending.count).to eq(1)
+ expect(described_class.status_processed.count).to eq(3)
+ end
end
end
- describe '.mark_records_processed' do
- it 'updates all records' do
- described_class.mark_records_processed([deleted_record_1, deleted_record_2, deleted_record_4])
+ describe 'sliding_list partitioning' do
+ let(:connection) { described_class.connection }
+ let(:partition_manager) { Gitlab::Database::Partitioning::PartitionManager.new(described_class) }
+
+ describe 'next_partition_if callback' do
+ let(:active_partition) { described_class.partitioning_strategy.active_partition.value }
+
+ subject(:value) { described_class.partitioning_strategy.next_partition_if.call(active_partition) }
+
+ context 'when the partition is empty' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when the partition has records' do
+ before do
+ described_class.create!(fully_qualified_table_name: 'public.table', primary_key_value: 1, status: :processed)
+ described_class.create!(fully_qualified_table_name: 'public.table', primary_key_value: 2, status: :pending)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when the first record of the partition is older than PARTITION_DURATION' do
+ before do
+ described_class.create!(
+ fully_qualified_table_name: 'public.table',
+ primary_key_value: 1,
+ created_at: (described_class::PARTITION_DURATION + 1.day).ago)
+
+ described_class.create!(fully_qualified_table_name: 'public.table', primary_key_value: 2)
+ end
+
+ it { is_expected.to eq(true) }
+
+ context 'when the lfk_automatic_partition_creation FF is off' do
+ before do
+ stub_feature_flags(lfk_automatic_partition_creation: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe 'detach_partition_if callback' do
+ let(:active_partition) { described_class.partitioning_strategy.active_partition.value }
+
+ subject(:value) { described_class.partitioning_strategy.detach_partition_if.call(active_partition) }
+
+ context 'when the partition contains unprocessed records' do
+ before do
+ described_class.create!(fully_qualified_table_name: 'public.table', primary_key_value: 1, status: :processed)
+ described_class.create!(fully_qualified_table_name: 'public.table', primary_key_value: 2, status: :pending)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when the partition contains only processed records' do
+ before do
+ described_class.create!(fully_qualified_table_name: 'public.table', primary_key_value: 1, status: :processed)
+ described_class.create!(fully_qualified_table_name: 'public.table', primary_key_value: 2, status: :processed)
+ end
+
+ it { is_expected.to eq(true) }
+
+ context 'when the lfk_automatic_partition_dropping FF is off' do
+ before do
+ stub_feature_flags(lfk_automatic_partition_dropping: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe 'the behavior of the strategy' do
+ it 'moves records to new partitions as time passes', :freeze_time do
+ # We start with partition 1
+ expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to eq([1])
+
+ # it's not a day old yet so no new partitions are created
+ partition_manager.sync_partitions
+
+ expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to eq([1])
+
+ # add one record so the next partition will be created
+ described_class.create!(fully_qualified_table_name: 'public.table', primary_key_value: 1)
+
+ # after traveling forward a day
+ travel(described_class::PARTITION_DURATION + 1.second)
+
+ # a new partition is created
+ partition_manager.sync_partitions
+
+ expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to eq([1, 2])
+
+ # and we can insert to the new partition
+ expect { described_class.create!(fully_qualified_table_name: table, primary_key_value: 5) }.not_to raise_error
+
+ # after processing old records
+ LooseForeignKeys::DeletedRecord.for_partition(1).update_all(status: :processed)
+
+ partition_manager.sync_partitions
+
+ # the old one is removed
+ expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to eq([2])
- expect(described_class.status_pending.count).to eq(1)
- expect(described_class.status_processed.count).to eq(3)
+ # and we only have the newly created partition left.
+ expect(described_class.count).to eq(1)
+ end
end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 0130dd7da4b..54327fc70d9 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -2059,4 +2059,75 @@ RSpec.describe Namespace do
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :group }
end
+
+ context 'Namespaces::SyncEvent' do
+ let!(:namespace) { create(:group) }
+
+ let_it_be(:new_namespace1) { create(:group) }
+ let_it_be(:new_namespace2) { create(:group) }
+
+ context 'when creating the namespace' do
+ it 'creates a namespaces_sync_event record' do
+ expect(namespace.sync_events.count).to eq(1)
+ end
+
+ it 'enqueues ProcessSyncEventsWorker' do
+ expect(Namespaces::ProcessSyncEventsWorker).to receive(:perform_async)
+
+ create(:namespace)
+ end
+ end
+
+ context 'when updating namespace parent_id' do
+ it 'creates a namespaces_sync_event record' do
+ expect do
+ namespace.update!(parent_id: new_namespace1.id)
+ end.to change(Namespaces::SyncEvent, :count).by(1)
+
+ expect(namespace.sync_events.count).to eq(2)
+ end
+
+ it 'enqueues ProcessSyncEventsWorker' do
+ expect(Namespaces::ProcessSyncEventsWorker).to receive(:perform_async)
+
+ namespace.update!(parent_id: new_namespace1.id)
+ end
+ end
+
+ context 'when updating namespace other attribute' do
+ it 'creates a namespaces_sync_event record' do
+ expect do
+ namespace.update!(name: 'hello')
+ end.not_to change(Namespaces::SyncEvent, :count)
+ end
+ end
+
+ context 'in the same transaction' do
+ context 'when updating different parent_id' do
+ it 'creates two namespaces_sync_event records' do
+ expect do
+ Namespace.transaction do
+ namespace.update!(parent_id: new_namespace1.id)
+ namespace.update!(parent_id: new_namespace2.id)
+ end
+ end.to change(Namespaces::SyncEvent, :count).by(2)
+
+ expect(namespace.sync_events.count).to eq(3)
+ end
+ end
+
+ context 'when updating the same parent_id' do
+ it 'creates one namespaces_sync_event record' do
+ expect do
+ Namespace.transaction do
+ namespace.update!(parent_id: new_namespace1.id)
+ namespace.update!(parent_id: new_namespace1.id)
+ end
+ end.to change(Namespaces::SyncEvent, :count).by(1)
+
+ expect(namespace.sync_events.count).to eq(2)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 5cdcffb98d1..4e38bf7d3e3 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -7480,6 +7480,77 @@ RSpec.describe Project, factory_default: :keep do
let(:factory_name) { :project }
end
+ context 'Projects::SyncEvent' do
+ let!(:project) { create(:project) }
+
+ let_it_be(:new_namespace1) { create(:namespace) }
+ let_it_be(:new_namespace2) { create(:namespace) }
+
+ context 'when creating the project' do
+ it 'creates a projects_sync_event record' do
+ expect(project.sync_events.count).to eq(1)
+ end
+
+ it 'enqueues ProcessProjectSyncEventsWorker' do
+ expect(Projects::ProcessSyncEventsWorker).to receive(:perform_async)
+
+ create(:project)
+ end
+ end
+
+ context 'when updating project namespace_id' do
+ it 'creates a projects_sync_event record' do
+ expect do
+ project.update!(namespace_id: new_namespace1.id)
+ end.to change(Projects::SyncEvent, :count).by(1)
+
+ expect(project.sync_events.count).to eq(2)
+ end
+
+ it 'enqueues ProcessProjectSyncEventsWorker' do
+ expect(Projects::ProcessSyncEventsWorker).to receive(:perform_async)
+
+ project.update!(namespace_id: new_namespace1.id)
+ end
+ end
+
+ context 'when updating project other attribute' do
+ it 'creates a projects_sync_event record' do
+ expect do
+ project.update!(name: 'hello')
+ end.not_to change(Projects::SyncEvent, :count)
+ end
+ end
+
+ context 'in the same transaction' do
+ context 'when updating different namespace_id' do
+ it 'creates two projects_sync_event records' do
+ expect do
+ Project.transaction do
+ project.update!(namespace_id: new_namespace1.id)
+ project.update!(namespace_id: new_namespace2.id)
+ end
+ end.to change(Projects::SyncEvent, :count).by(2)
+
+ expect(project.sync_events.count).to eq(3)
+ end
+ end
+
+ context 'when updating the same namespace_id' do
+ it 'creates one projects_sync_event record' do
+ expect do
+ Project.transaction do
+ project.update!(namespace_id: new_namespace1.id)
+ project.update!(namespace_id: new_namespace1.id)
+ end
+ end.to change(Projects::SyncEvent, :count).by(1)
+
+ expect(project.sync_events.count).to eq(2)
+ end
+ end
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index acc3d598269..d10f1405a7b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1540,7 +1540,11 @@ RSpec.describe User do
allow(user).to receive(:update_highest_role)
end
- expect(SecureRandom).to receive(:hex).and_return('3b8ca303')
+ allow_next_instance_of(Namespaces::UserNamespace) do |namespace|
+ allow(namespace).to receive(:schedule_sync_event_worker)
+ end
+
+ expect(SecureRandom).to receive(:hex).with(no_args).and_return('3b8ca303')
user = create(:user)
diff --git a/spec/policies/namespaces/user_namespace_policy_spec.rb b/spec/policies/namespaces/user_namespace_policy_spec.rb
index af20982ed0c..06db2f6e243 100644
--- a/spec/policies/namespaces/user_namespace_policy_spec.rb
+++ b/spec/policies/namespaces/user_namespace_policy_spec.rb
@@ -74,4 +74,26 @@ RSpec.describe Namespaces::UserNamespacePolicy do
it { is_expected.to be_disallowed(:create_jira_connect_subscription) }
end
end
+
+ describe 'create projects' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:current_user) { owner }
+
+ context 'when user can create projects' do
+ before do
+ allow(current_user).to receive(:can_create_project?).and_return(true)
+ end
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context 'when user cannot create projects' do
+ before do
+ allow(current_user).to receive(:can_create_project?).and_return(false)
+ end
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+ end
end
diff --git a/spec/requests/api/error_tracking/collector_spec.rb b/spec/requests/api/error_tracking/collector_spec.rb
index 21e2849fef0..573da862b57 100644
--- a/spec/requests/api/error_tracking/collector_spec.rb
+++ b/spec/requests/api/error_tracking/collector_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe API::ErrorTracking::Collector do
end
RSpec.shared_examples 'successful request' do
- it 'writes to the database and returns OK' do
+ it 'writes to the database and returns OK', :aggregate_failures do
expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
@@ -40,6 +40,8 @@ RSpec.describe API::ErrorTracking::Collector do
subject { post api(url), params: params, headers: headers }
+ it_behaves_like 'successful request'
+
context 'error tracking feature is disabled' do
before do
setting.update!(enabled: false)
@@ -109,8 +111,6 @@ RSpec.describe API::ErrorTracking::Collector do
it_behaves_like 'successful request'
end
-
- it_behaves_like 'successful request'
end
describe "POST /error_tracking/collector/api/:id/store" do
@@ -165,6 +165,12 @@ RSpec.describe API::ErrorTracking::Collector do
it_behaves_like 'successful request'
end
+ context 'body contains nullbytes' do
+ let_it_be(:raw_event) { fixture_file('error_tracking/parsed_event_nullbytes.json') }
+
+ it_behaves_like 'successful request'
+ end
+
context 'sentry_key as param and empty headers' do
let(:url) { "/error_tracking/collector/api/#{project.id}/store?sentry_key=#{sentry_key}" }
let(:headers) { {} }
diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb
index 6d8ae226ce4..838948132dd 100644
--- a/spec/requests/api/v3/github_spec.rb
+++ b/spec/requests/api/v3/github_spec.rb
@@ -567,18 +567,6 @@ RSpec.describe API::V3::Github do
expect(response_diff_files(response)).to be_blank
end
- it 'does not handle the error when feature flag is disabled', :aggregate_failures do
- stub_feature_flags(api_v3_commits_skip_diff_files: false)
-
- allow(Gitlab::GitalyClient).to receive(:call)
- .with(*commit_diff_args)
- .and_raise(GRPC::DeadlineExceeded)
-
- call_api
-
- expect(response).to have_gitlab_http_status(:error)
- end
-
it 'only calls Gitaly once for all attempts within a period of time', :aggregate_failures do
expect(Gitlab::GitalyClient).to receive(:call)
.with(*commit_diff_args)
diff --git a/spec/services/ci/process_sync_events_service_spec.rb b/spec/services/ci/process_sync_events_service_spec.rb
new file mode 100644
index 00000000000..00b670ff54f
--- /dev/null
+++ b/spec/services/ci/process_sync_events_service_spec.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::ProcessSyncEventsService do
+ let!(:group) { create(:group) }
+ let!(:project1) { create(:project, group: group) }
+ let!(:project2) { create(:project, group: group) }
+ let!(:parent_group_1) { create(:group) }
+ let!(:parent_group_2) { create(:group) }
+
+ subject(:service) { described_class.new(sync_event_class, hierarchy_class) }
+
+ describe '#perform' do
+ subject(:execute) { service.execute }
+
+ context 'for Projects::SyncEvent' do
+ let(:sync_event_class) { Projects::SyncEvent }
+ let(:hierarchy_class) { ::Ci::ProjectMirror }
+
+ before do
+ Projects::SyncEvent.delete_all
+
+ project1.update!(group: parent_group_1)
+ project2.update!(group: parent_group_2)
+ end
+
+ it 'consumes events' do
+ expect { execute }.to change(Projects::SyncEvent, :count).from(2).to(0)
+
+ expect(project1.ci_project_mirror).to have_attributes(
+ namespace_id: parent_group_1.id
+ )
+ expect(project2.ci_project_mirror).to have_attributes(
+ namespace_id: parent_group_2.id
+ )
+ end
+
+ it 'enqueues Projects::ProcessSyncEventsWorker if any left' do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+
+ expect(Projects::ProcessSyncEventsWorker).to receive(:perform_async)
+
+ execute
+ end
+
+ it 'does not enqueue Projects::ProcessSyncEventsWorker if no left' do
+ stub_const("#{described_class}::BATCH_SIZE", 2)
+
+ expect(Projects::ProcessSyncEventsWorker).not_to receive(:perform_async)
+
+ execute
+ end
+
+ context 'when there is no event' do
+ before do
+ Projects::SyncEvent.delete_all
+ end
+
+ it 'does nothing' do
+ expect { execute }.not_to change(Projects::SyncEvent, :count)
+ end
+ end
+
+ context 'when the FF ci_namespace_project_mirrors is disabled' do
+ before do
+ stub_feature_flags(ci_namespace_project_mirrors: false)
+ end
+
+ it 'does nothing' do
+ expect { execute }.not_to change(Projects::SyncEvent, :count)
+ end
+ end
+ end
+
+ context 'for Namespaces::SyncEvent' do
+ let(:sync_event_class) { Namespaces::SyncEvent }
+ let(:hierarchy_class) { ::Ci::NamespaceMirror }
+
+ before do
+ Namespaces::SyncEvent.delete_all
+
+ group.update!(parent: parent_group_2)
+ parent_group_2.update!(parent: parent_group_1)
+ end
+
+ shared_examples 'event consuming' do
+ it 'consumes events' do
+ expect { execute }.to change(Namespaces::SyncEvent, :count).from(2).to(0)
+
+ expect(group.ci_namespace_mirror).to have_attributes(
+ traversal_ids: [parent_group_1.id, parent_group_2.id, group.id]
+ )
+ expect(parent_group_2.ci_namespace_mirror).to have_attributes(
+ traversal_ids: [parent_group_1.id, parent_group_2.id]
+ )
+ end
+ end
+
+ context 'when the FFs sync_traversal_ids, use_traversal_ids and use_traversal_ids_for_ancestors are disabled' do
+ before do
+ stub_feature_flags(sync_traversal_ids: false,
+ use_traversal_ids: false,
+ use_traversal_ids_for_ancestors: false)
+ end
+
+ it_behaves_like 'event consuming'
+ end
+
+ it_behaves_like 'event consuming'
+
+ it 'enqueues Namespaces::ProcessSyncEventsWorker if any left' do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+
+ expect(Namespaces::ProcessSyncEventsWorker).to receive(:perform_async)
+
+ execute
+ end
+
+ it 'does not enqueue Namespaces::ProcessSyncEventsWorker if no left' do
+ stub_const("#{described_class}::BATCH_SIZE", 2)
+
+ expect(Namespaces::ProcessSyncEventsWorker).not_to receive(:perform_async)
+
+ execute
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 817fed06b50..e3e2f5b59da 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -128,25 +128,9 @@ RSpec.describe Ci::RetryBuildService do
expect(new_build.needs).not_to match(build.needs)
end
- context 'when clone_job_variables_at_job_retry is enabled' do
- before do
- stub_feature_flags(clone_job_variables_at_job_retry: true)
- end
-
- it 'clones only internal job variables' do
- expect(new_build.job_variables.count).to eq(1)
- expect(new_build.job_variables).to contain_exactly(having_attributes(key: internal_job_variable.key, value: internal_job_variable.value))
- end
- end
-
- context 'when clone_job_variables_at_job_retry is not enabled' do
- before do
- stub_feature_flags(clone_job_variables_at_job_retry: false)
- end
-
- it 'does not clone internal job variables' do
- expect(new_build.job_variables.count).to eq(0)
- end
+ it 'clones only internal job variables' do
+ expect(new_build.job_variables.count).to eq(1)
+ expect(new_build.job_variables).to contain_exactly(having_attributes(key: internal_job_variable.key, value: internal_job_variable.value))
end
end
@@ -170,7 +154,7 @@ RSpec.describe Ci::RetryBuildService do
Ci::Build.attribute_names.map(&:to_sym) +
Ci::Build.attribute_aliases.keys.map(&:to_sym) +
Ci::Build.reflect_on_all_associations.map(&:name) +
- [:tag_list, :needs_attributes] -
+ [:tag_list, :needs_attributes, :job_variables_attributes] -
# ee-specific accessors should be tested in ee/spec/services/ci/retry_build_service_spec.rb instead
described_class.extra_accessors -
[:dast_site_profiles_build, :dast_scanner_profiles_build] # join tables
diff --git a/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb b/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb
index ac9b01be0b5..d3d57ea2444 100644
--- a/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb
+++ b/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb
@@ -90,7 +90,7 @@ RSpec.describe LooseForeignKeys::BatchCleanerService do
described_class.new(parent_table: '_test_loose_fk_parent_table',
loose_foreign_key_definitions: loose_foreign_key_definitions,
- deleted_parent_records: LooseForeignKeys::DeletedRecord.status_pending.all
+ deleted_parent_records: LooseForeignKeys::DeletedRecord.load_batch_for_table('public._test_loose_fk_parent_table', 100)
).execute
end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index ad85e97c333..d7a36ff370e 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -569,6 +569,27 @@ RSpec.describe SearchService do
end
end
+ describe '#abuse_messages' do
+ let(:scope) { 'issues' }
+ let(:search) { 'foobar' }
+ let(:params) { instance_double(Gitlab::Search::Params) }
+
+ before do
+ allow(Gitlab::Search::Params).to receive(:new).and_return(params)
+ end
+
+ it 'returns an empty array when not abusive' do
+ allow(params).to receive(:abusive?).and_return false
+ expect(subject.abuse_messages).to match_array([])
+ end
+
+ it 'calls on abuse_detection.errors.full_messages when abusive' do
+ allow(params).to receive(:abusive?).and_return true
+ expect(params).to receive_message_chain(:abuse_detection, :errors, :full_messages)
+ subject.abuse_messages
+ end
+ end
+
describe 'abusive search handling' do
subject { described_class.new(user, raw_params) }
diff --git a/spec/workers/namespaces/process_sync_events_worker_spec.rb b/spec/workers/namespaces/process_sync_events_worker_spec.rb
new file mode 100644
index 00000000000..59be1fffdb4
--- /dev/null
+++ b/spec/workers/namespaces/process_sync_events_worker_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::ProcessSyncEventsWorker do
+ let!(:group1) { create(:group) }
+ let!(:group2) { create(:group) }
+ let!(:group3) { create(:group) }
+
+ include_examples 'an idempotent worker'
+
+ describe '#perform' do
+ subject(:perform) { described_class.new.perform }
+
+ before do
+ group2.update!(parent: group1)
+ group3.update!(parent: group2)
+ end
+
+ it 'consumes all sync events' do
+ expect { perform }.to change(Namespaces::SyncEvent, :count).from(5).to(0)
+ end
+
+ it 'syncs namespace hierarchy traversal ids' do
+ expect { perform }.to change(Ci::NamespaceMirror, :all).to contain_exactly(
+ an_object_having_attributes(namespace_id: group1.id, traversal_ids: [group1.id]),
+ an_object_having_attributes(namespace_id: group2.id, traversal_ids: [group1.id, group2.id]),
+ an_object_having_attributes(namespace_id: group3.id, traversal_ids: [group1.id, group2.id, group3.id])
+ )
+ end
+ end
+end
diff --git a/spec/workers/projects/process_sync_events_worker_spec.rb b/spec/workers/projects/process_sync_events_worker_spec.rb
new file mode 100644
index 00000000000..600fbbc6b20
--- /dev/null
+++ b/spec/workers/projects/process_sync_events_worker_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ProcessSyncEventsWorker do
+ let!(:group) { create(:group) }
+ let!(:project) { create(:project) }
+
+ include_examples 'an idempotent worker'
+
+ describe '#perform' do
+ subject(:perform) { described_class.new.perform }
+
+ before do
+ project.update!(namespace: group)
+ end
+
+ it 'consumes all sync events' do
+ expect { perform }.to change(Projects::SyncEvent, :count).from(2).to(0)
+ end
+
+ it 'syncs project namespace id' do
+ expect { perform }.to change(Ci::ProjectMirror, :all).to contain_exactly(
+ an_object_having_attributes(namespace_id: group.id)
+ )
+ end
+ end
+end