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>2024-01-09 21:07:32 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-09 21:07:32 +0300
commitcd22685717501ac6f3c81e16418270123a2cccee (patch)
treeeb45b2ea1c3ac174aaa4fd54a38901f1f8420838
parent1f753bca2624be1e507424127fe0d48b9765da70 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab-ci.yml12
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml3
-rw-r--r--.rubocop_todo/rspec/before_all_role_assignment.yml3
-rw-r--r--.rubocop_todo/rspec/named_subject.yml1
-rw-r--r--.rubocop_todo/style/inline_disable_annotation.yml1
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_tabs.vue54
-rw-r--r--app/assets/javascripts/environments/helpers/k8s_integration_helper.js11
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue5
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue7
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/constants.js33
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/client.js14
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js18
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql17
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js40
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js11
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/pages/services_page.vue69
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/router/constants.js2
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/router/routes.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_countdown.vue5
-rw-r--r--app/graphql/types/ci/ci_cd_setting_type.rb2
-rw-r--r--app/models/member.rb14
-rw-r--r--app/models/users/phone_number_validation.rb18
-rw-r--r--app/policies/organizations/organization_policy.rb8
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/click_house/event_authors_consistency_cron_worker.rb121
-rw-r--r--config/feature_flags/development/sms_send_wait_time.yml8
-rw-r--r--config/feature_flags/gitlab_com_derisk/update_organization_users.yml9
-rw-r--r--config/feature_flags/ops/ai_duo_chat_switch.yml9
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/macos.rb8
-rw-r--r--db/migrate/20231124022520_add_sms_sent_at_and_sms_send_count_to_phone_number_validations.rb16
-rw-r--r--db/migrate/20240101031938_add_admin_terraform_state_to_member_roles.rb10
-rw-r--r--db/post_migrate/20240109025151_create_index_on_id_convert_to_bigint_for_system_note_metadata_async.rb16
-rw-r--r--db/schema_migrations/202311240225201
-rw-r--r--db/schema_migrations/202401010319381
-rw-r--r--db/schema_migrations/202401090251511
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/admin_area.md9
-rw-r--r--doc/api/graphql/reference/index.md7
-rw-r--r--doc/api/member_roles.md8
-rw-r--r--doc/api/projects.md4
-rw-r--r--doc/api/statistics.md3
-rw-r--r--doc/development/fe_guide/frontend_faq.md7
-rw-r--r--doc/development/testing_guide/end_to_end/execution_context_selection.md1
-rw-r--r--doc/user/application_security/dependency_scanning/index.md14
-rw-r--r--doc/user/project/pages/redirects.md26
-rw-r--r--doc/user/project/repository/branches/index.md2
-rw-r--r--lib/click_house/iterator.rb19
-rw-r--r--lib/gitlab/ci/parsers/security/common.rb1
-rw-r--r--lib/gitlab/ci/reports/security/finding.rb12
-rw-r--r--lib/gitlab/usage_data_counters/ci_template_unique_counter.rb15
-rw-r--r--lib/tasks/gitlab/usage_data.rake34
-rw-r--r--locale/gitlab.pot25
-rw-r--r--qa/Dockerfile2
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--qa/qa/runtime/env.rb2
-rw-r--r--qa/qa/service/docker_run/gitlab.rb14
-rw-r--r--qa/qa/service/gitlab/instances.rb84
-rw-r--r--qa/qa/specs/features/browser_ui/9_data_stores/cells/login_across_multiple_cells_spec.rb50
-rw-r--r--qa/qa/specs/features/browser_ui/9_data_stores/cells/multiple_cells_gdk_spec.rb23
-rw-r--r--qa/qa/specs/helpers/context_selector.rb5
-rw-r--r--qa/spec/specs/helpers/context_selector_spec.rb21
-rw-r--r--scripts/rspec_helpers.sh13
-rw-r--r--spec/factories/ci/reports/security/findings.rb1
-rw-r--r--spec/factories/groups.rb4
-rw-r--r--spec/features/groups_spec.rb19
-rw-r--r--spec/frontend/environments/helpers/k8s_integration_helper_spec.js30
-rw-r--r--spec/frontend/kubernetes_dashboard/graphql/mock_data.js84
-rw-r--r--spec/frontend/kubernetes_dashboard/graphql/resolvers/kubernetes_spec.js84
-rw-r--r--spec/frontend/kubernetes_dashboard/helpers/k8s_integration_helper_spec.js30
-rw-r--r--spec/frontend/kubernetes_dashboard/pages/services_page_spec.js104
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js4
-rw-r--r--spec/helpers/ci/catalog/resources_helper_spec.rb12
-rw-r--r--spec/lib/click_house/iterator_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/reports/security/report_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb12
-rw-r--r--spec/models/member_spec.rb104
-rw-r--r--spec/models/users/phone_number_validation_spec.rb41
-rw-r--r--spec/policies/organizations/organization_policy_spec.rb15
-rw-r--r--spec/requests/api/graphql/mutations/organizations/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/organizations/organization_query_spec.rb4
-rw-r--r--spec/requests/organizations/settings_controller_spec.rb19
-rw-r--r--spec/services/ci/catalog/resources/create_service_spec.rb4
-rw-r--r--spec/services/ci/catalog/resources/destroy_service_spec.rb4
-rw-r--r--spec/services/organizations/update_service_spec.rb2
-rw-r--r--spec/services/security/merge_reports_service_spec.rb56
-rw-r--r--spec/validators/ip_cidr_array_validator_spec.rb1
-rw-r--r--spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb104
92 files changed, 1479 insertions, 276 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6acf982fd55..bb29a7bb930 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -40,11 +40,12 @@ default:
OMNIBUS_GITLAB_CACHE_EDITION: "GITLAB_RUBY3_2"
.default-branch-pipeline-failure-variables: &default-branch-pipeline-failure-variables
- CREATE_RAILS_TEST_FAILURE_ISSUES: "true"
CREATE_RAILS_SLOW_TEST_ISSUES: "true"
+ CREATE_RAILS_TEST_FAILURE_ISSUES: "true"
-.default-merge-request-slow-tests-variables: &default-merge-request-slow-tests-variables
+.default-merge-request-variables: &default-merge-request-variables
ADD_SLOW_TEST_NOTE_TO_MERGE_REQUEST: "true"
+ CREATE_RAILS_FLAKY_TEST_ISSUES: "true"
.if-merge-request-security-canonical-sync: &if-merge-request-security-canonical-sync
if: '$CI_MERGE_REQUEST_SOURCE_PROJECT_PATH == "gitlab-org/security/gitlab" && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == $CI_DEFAULT_BRANCH && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH'
@@ -104,8 +105,7 @@ workflow:
# For (detached) merge request pipelines.
- if: '$CI_MERGE_REQUEST_IID'
variables:
- <<: *default-ruby-variables
- <<: *default-merge-request-slow-tests-variables
+ <<: [*default-ruby-variables, *default-merge-request-variables]
PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
NO_SOURCEMAPS: 'true'
# For the scheduled pipelines, we set specific variables.
@@ -122,7 +122,7 @@ workflow:
variables:
<<: *next-ruby-variables
PIPELINE_NAME: 'Scheduled Ruby $RUBY_VERSION $CI_COMMIT_BRANCH branch pipeline'
- # This work around https://gitlab.com/gitlab-org/gitlab/-/issues/332411 whichs prevents usage of dependency proxy
+ # This work around https://gitlab.com/gitlab-org/gitlab/-/issues/332411 which prevents usage of dependency proxy
# when pipeline is triggered by a project access token.
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $GITLAB_USER_LOGIN =~ /project_\d+_bot\d*/'
variables:
@@ -183,7 +183,7 @@ variables:
CI_FETCH_REPO_GIT_STRATEGY: "none"
DEBIAN_VERSION: "bullseye"
UBI_VERSION: "8.6"
- CHROME_VERSION: "113"
+ CHROME_VERSION: "119"
DOCKER_VERSION: "24.0.5"
RUBYGEMS_VERSION: "3.4"
GO_VERSION: "1.20"
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index 2e963b7857a..2292c18e379 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -80,7 +80,7 @@ gemnasium-python-dependency_scanning:
extends: .default-retry
stage: test
image:
- name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/security-products/package-hunter-cli:v3.0.0@sha256:e281525b3be870d6618b6bad2685733dcb9908e4eb21f0e5b4fe4bb6f6083f91
+ name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/security-products/package-hunter-cli:v3.0.1@sha256:ffa4af2810fed6922ba9d19badc4636043f54f70db19aebb8253e83142e5da16
entrypoint: [""]
variables:
HTR_user: '$PACKAGE_HUNTER_USER'
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 1159bccb114..d2bbc22625c 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -87,7 +87,6 @@ start-review-app-pipeline:
# https://gitlab.com/gitlab-org/gitlab/-/issues/387183
inherit:
variables:
- - CHROME_VERSION
- REGISTRY_GROUP
- REGISTRY_HOST
- REVIEW_APPS_DOMAIN
@@ -96,6 +95,8 @@ start-review-app-pipeline:
- REVIEW_APPS_IMAGE
- RUBY_VERSION
- DEBIAN_VERSION
+ - DOCKER_VERSION
+ - CHROME_VERSION
# These variables are set in the pipeline schedules.
# They need to be explicitly passed on to the child pipeline.
diff --git a/.rubocop_todo/rspec/before_all_role_assignment.yml b/.rubocop_todo/rspec/before_all_role_assignment.yml
index a39743664aa..8e1a2a28f1f 100644
--- a/.rubocop_todo/rspec/before_all_role_assignment.yml
+++ b/.rubocop_todo/rspec/before_all_role_assignment.yml
@@ -68,7 +68,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/features/boards/sidebar_spec.rb'
- 'ee/spec/features/boards/swimlanes/epics_swimlanes_sidebar_spec.rb'
- 'ee/spec/features/burnup_charts_spec.rb'
- - 'ee/spec/features/ci/ci_catalog_spec.rb'
- 'ee/spec/features/dashboards/todos_spec.rb'
- 'ee/spec/features/epic_boards/epic_boards_sidebar_spec.rb'
- 'ee/spec/features/epic_boards/epic_boards_spec.rb'
@@ -265,7 +264,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/graphql/types/vulnerability_response_type_spec.rb'
- 'ee/spec/graphql/types/vulnerability_scanner_type_spec.rb'
- 'ee/spec/graphql/types/vulnerability_type_spec.rb'
- - 'ee/spec/helpers/ee/ci/catalog/resources_helper_spec.rb'
- 'ee/spec/helpers/ee/ci/pipeline_editor_helper_spec.rb'
- 'ee/spec/helpers/ee/environments_helper_spec.rb'
- 'ee/spec/helpers/ee/groups_helper_spec.rb'
@@ -552,7 +550,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/requests/jwt_controller_spec.rb'
- 'ee/spec/requests/lfs_locks_api_spec.rb'
- 'ee/spec/requests/projects/analytics/cycle_analytics/stages_controller_spec.rb'
- - 'ee/spec/requests/projects/ci/catalog/resources_controller_spec.rb'
- 'ee/spec/requests/projects/dependencies_controller_spec.rb'
- 'ee/spec/requests/projects/issues_controller_spec.rb'
- 'ee/spec/requests/projects/on_demand_scans_controller_spec.rb'
diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml
index 79afdfa04f6..2c77a52579c 100644
--- a/.rubocop_todo/rspec/named_subject.yml
+++ b/.rubocop_todo/rspec/named_subject.yml
@@ -193,7 +193,6 @@ RSpec/NamedSubject:
- 'ee/spec/helpers/compliance_management/compliance_framework/group_settings_helper_spec.rb'
- 'ee/spec/helpers/ee/auth_helper_spec.rb'
- 'ee/spec/helpers/ee/branches_helper_spec.rb'
- - 'ee/spec/helpers/ee/ci/catalog/resources_helper_spec.rb'
- 'ee/spec/helpers/ee/ci/runners_helper_spec.rb'
- 'ee/spec/helpers/ee/emails_helper_spec.rb'
- 'ee/spec/helpers/ee/environments_helper_spec.rb'
diff --git a/.rubocop_todo/style/inline_disable_annotation.yml b/.rubocop_todo/style/inline_disable_annotation.yml
index 98690775f3c..473378c24fb 100644
--- a/.rubocop_todo/style/inline_disable_annotation.yml
+++ b/.rubocop_todo/style/inline_disable_annotation.yml
@@ -1986,7 +1986,6 @@ Style/InlineDisableAnnotation:
- 'ee/spec/features/trials/saas/creation_with_one_existing_namespace_flow_spec.rb'
- 'ee/spec/finders/audit_event_finder_spec.rb'
- 'ee/spec/finders/ee/group_members_finder_spec.rb'
- - 'ee/spec/frontend/fixtures/ci_catalog_resources.rb'
- 'ee/spec/helpers/analytics/analytics_dashboards_helper_spec.rb'
- 'ee/spec/helpers/ee/dashboard_helper_spec.rb'
- 'ee/spec/helpers/ee/releases_helper_spec.rb'
diff --git a/app/assets/javascripts/environments/components/kubernetes_tabs.vue b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
index 0d80b1fd797..da37df3fae7 100644
--- a/app/assets/javascripts/environments/components/kubernetes_tabs.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
@@ -1,9 +1,12 @@
<script>
import { GlTabs, GlTab, GlLoadingIcon, GlBadge, GlTable, GlPagination } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import { getAge } from '~/kubernetes_dashboard/helpers/k8s_integration_helper';
+import {
+ getAge,
+ generateServicePortsString,
+} from '~/kubernetes_dashboard/helpers/k8s_integration_helper';
+import { SERVICES_TABLE_FIELDS } from '~/kubernetes_dashboard/constants';
import k8sServicesQuery from '../graphql/queries/k8s_services.query.graphql';
-import { generateServicePortsString } from '../helpers/k8s_integration_helper';
import { SERVICES_LIMIT_PER_PAGE } from '../constants';
import KubernetesSummary from './kubernetes_summary.vue';
@@ -82,6 +85,14 @@ export default {
? null
: nextPage;
},
+ servicesFields() {
+ return SERVICES_TABLE_FIELDS.map((field) => {
+ return {
+ ...field,
+ thClass: tableHeadingClasses,
+ };
+ });
+ },
},
i18n: {
servicesTitle: s__('Environment|Services'),
@@ -94,43 +105,6 @@ export default {
ports: s__('Environment|Ports'),
age: s__('Environment|Age'),
},
- servicesFields: [
- {
- key: 'name',
- label: __('Name'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'namespace',
- label: __('Namespace'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'type',
- label: __('Type'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'clusterIP',
- label: s__('Environment|Cluster IP'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'externalIP',
- label: s__('Environment|External IP'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'ports',
- label: s__('Environment|Ports'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'age',
- label: s__('Environment|Age'),
- thClass: tableHeadingClasses,
- },
- ],
SERVICES_LIMIT_PER_PAGE,
};
</script>
@@ -154,7 +128,7 @@ export default {
<gl-table
v-else
- :fields="$options.servicesFields"
+ :fields="servicesFields"
:items="servicesItems"
:per-page="$options.SERVICES_LIMIT_PER_PAGE"
:current-page="currentPage"
diff --git a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
index 8b907f0b174..99d5ee44b6c 100644
--- a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
+++ b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
@@ -13,17 +13,6 @@ import {
} from '~/kubernetes_dashboard/constants';
import { CLUSTER_AGENT_ERROR_MESSAGES } from '../constants';
-export function generateServicePortsString(ports) {
- if (!ports?.length) return '';
-
- return ports
- .map((port) => {
- const nodePort = port.nodePort ? `:${port.nodePort}` : '';
- return `${port.port}${nodePort}/${port.protocol}`;
- })
- .join(', ');
-}
-
export function getDeploymentsStatuses(items) {
const failed = [];
const ready = [];
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue
index 0d219f915c9..41fb2527036 100644
--- a/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue
+++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue
@@ -14,8 +14,7 @@ export default {
item: {
type: Object,
required: true,
- validator: (item) =>
- ['name', 'kind', 'labels', 'annotations', 'status'].every((key) => item[key]),
+ validator: (item) => ['name', 'kind', 'labels', 'annotations'].every((key) => item[key]),
},
},
computed: {
@@ -63,7 +62,7 @@ export default {
</gl-badge>
</div>
</workload-details-item>
- <workload-details-item :label="$options.i18n.status">
+ <workload-details-item v-if="item.status" :label="$options.i18n.status">
<gl-badge :variant="$options.WORKLOAD_STATUS_BADGE_VARIANTS[item.status]">{{
item.status
}}</gl-badge></workload-details-item
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue
index 8c6a08ad504..8b1436b5486 100644
--- a/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue
+++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue
@@ -33,6 +33,11 @@ export default {
type: Array,
required: true,
},
+ fields: {
+ type: Array,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -59,7 +64,7 @@ export default {
</gl-alert>
<div v-else>
<workload-stats :stats="stats" />
- <workload-table :items="items" @select-item="onItemSelect" />
+ <workload-table :items="items" :fields="fields" @select-item="onItemSelect" />
<gl-drawer
:open="showDetailsDrawer"
diff --git a/app/assets/javascripts/kubernetes_dashboard/constants.js b/app/assets/javascripts/kubernetes_dashboard/constants.js
index cc554722bba..0696fcab875 100644
--- a/app/assets/javascripts/kubernetes_dashboard/constants.js
+++ b/app/assets/javascripts/kubernetes_dashboard/constants.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
export const STATUS_RUNNING = 'Running';
export const STATUS_PENDING = 'Pending';
@@ -53,3 +53,34 @@ export const DEFAULT_WORKLOAD_TABLE_FIELDS = [
export const STATUS_TRUE = 'True';
export const STATUS_FALSE = 'False';
+
+export const SERVICES_TABLE_FIELDS = [
+ {
+ key: 'name',
+ label: __('Name'),
+ },
+ {
+ key: 'namespace',
+ label: __('Namespace'),
+ },
+ {
+ key: 'type',
+ label: __('Type'),
+ },
+ {
+ key: 'clusterIP',
+ label: s__('Environment|Cluster IP'),
+ },
+ {
+ key: 'externalIP',
+ label: s__('Environment|External IP'),
+ },
+ {
+ key: 'ports',
+ label: s__('Environment|Ports'),
+ },
+ {
+ key: 'age',
+ label: s__('Environment|Age'),
+ },
+];
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/client.js b/app/assets/javascripts/kubernetes_dashboard/graphql/client.js
index 4a1ab56a8e9..9454465df9d 100644
--- a/app/assets/javascripts/kubernetes_dashboard/graphql/client.js
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/client.js
@@ -8,6 +8,7 @@ import k8sReplicaSetsQuery from './queries/k8s_dashboard_replica_sets.query.grap
import k8sDaemonSetsQuery from './queries/k8s_dashboard_daemon_sets.query.graphql';
import k8sJobsQuery from './queries/k8s_dashboard_jobs.query.graphql';
import k8sCronJobsQuery from './queries/k8s_dashboard_cron_jobs.query.graphql';
+import k8sServicesQuery from './queries/k8s_dashboard_services.query.graphql';
import { resolvers } from './resolvers';
export const apolloProvider = () => {
@@ -110,6 +111,19 @@ export const apolloProvider = () => {
},
});
+ cache.writeQuery({
+ query: k8sServicesQuery,
+ data: {
+ metadata,
+ spec: {
+ type: null,
+ clusterIP: null,
+ externalIP: null,
+ ports: null,
+ },
+ },
+ });
+
return new VueApollo({
defaultClient,
});
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
index a06883a0b24..b9c195d83d0 100644
--- a/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
@@ -62,6 +62,24 @@ export const mapJobItem = (item) => {
};
};
+export const mapServicesItems = (item) => {
+ const { type, clusterIP, externalIP, ports } = item.spec;
+
+ return {
+ metadata: {
+ ...item.metadata,
+ annotations: item.metadata?.annotations || {},
+ labels: item.metadata?.labels || {},
+ },
+ spec: {
+ type,
+ clusterIP: clusterIP || '-',
+ externalIP: externalIP || '-',
+ ports,
+ },
+ };
+};
+
export const mapCronJobItem = (item) => {
const metadata = {
...item.metadata,
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql
new file mode 100644
index 00000000000..7d42d66183e
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql
@@ -0,0 +1,17 @@
+query getK8sDashboardServices($configuration: LocalConfiguration) {
+ k8sServices(configuration: $configuration) @client {
+ metadata {
+ name
+ namespace
+ creationTimestamp
+ labels
+ annotations
+ }
+ spec {
+ type
+ clusterIP
+ externalIP
+ ports
+ }
+ }
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js
index 3450e2780cb..75285ad2cca 100644
--- a/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js
@@ -1,4 +1,4 @@
-import { Configuration, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
+import { Configuration, CoreV1Api, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
import {
getK8sPods,
@@ -9,6 +9,7 @@ import {
watchWorkloadItems,
mapJobItem,
mapCronJobItem,
+ mapServicesItems,
} from '../helpers/resolver_helpers';
import k8sDashboardPodsQuery from '../queries/k8s_dashboard_pods.query.graphql';
import k8sDashboardDeploymentsQuery from '../queries/k8s_dashboard_deployments.query.graphql';
@@ -17,6 +18,7 @@ import k8sDashboardReplicaSetsQuery from '../queries/k8s_dashboard_replica_sets.
import k8sDaemonSetsQuery from '../queries/k8s_dashboard_daemon_sets.query.graphql';
import k8sJobsQuery from '../queries/k8s_dashboard_jobs.query.graphql';
import k8sCronJobsQuery from '../queries/k8s_dashboard_cron_jobs.query.graphql';
+import k8sServicesQuery from '../queries/k8s_dashboard_services.query.graphql';
export default {
k8sPods(_, { configuration }, { client }) {
@@ -244,4 +246,40 @@ export default {
}
});
},
+
+ k8sServices(_, { configuration, namespace = '' }, { client }) {
+ const config = new Configuration(configuration);
+
+ const coreV1Api = new CoreV1Api(config);
+ const servicesApi = namespace
+ ? coreV1Api.listCoreV1NamespacedService({ namespace })
+ : coreV1Api.listCoreV1ServiceForAllNamespaces();
+ return servicesApi
+ .then((res) => {
+ const watchPath = buildWatchPath({
+ resource: 'services',
+ namespace,
+ });
+ watchWorkloadItems({
+ client,
+ query: k8sServicesQuery,
+ configuration,
+ namespace,
+ watchPath,
+ queryField: 'k8sServices',
+ mapFn: mapServicesItems,
+ });
+
+ const data = res?.items || [];
+
+ return data.map(mapServicesItems);
+ })
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ });
+ },
};
diff --git a/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js b/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js
index 25135e23dc8..d3116fd611a 100644
--- a/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js
+++ b/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js
@@ -77,3 +77,14 @@ export function calculateCronJobStatus(item) {
}
return STATUS_READY;
}
+
+export function generateServicePortsString(ports) {
+ if (!ports?.length) return '';
+
+ return ports
+ .map((port) => {
+ const nodePort = port.nodePort ? `:${port.nodePort}` : '';
+ return `${port.port}${nodePort}/${port.protocol}`;
+ })
+ .join(', ');
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/services_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/services_page.vue
new file mode 100644
index 00000000000..4dc8fb6b6c0
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/pages/services_page.vue
@@ -0,0 +1,69 @@
+<script>
+import { s__ } from '~/locale';
+import { getAge, generateServicePortsString } from '../helpers/k8s_integration_helper';
+import { SERVICES_TABLE_FIELDS } from '../constants';
+import WorkloadLayout from '../components/workload_layout.vue';
+import k8sServicesQuery from '../graphql/queries/k8s_dashboard_services.query.graphql';
+
+export default {
+ components: {
+ WorkloadLayout,
+ },
+ inject: ['configuration'],
+ apollo: {
+ k8sServices: {
+ query: k8sServicesQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ };
+ },
+ update(data) {
+ return (
+ data?.k8sServices?.map((service) => {
+ return {
+ name: service.metadata?.name,
+ namespace: service.metadata?.namespace,
+ type: service.spec?.type,
+ clusterIP: service.spec?.clusterIP,
+ externalIP: service.spec?.externalIP,
+ ports: generateServicePortsString(service?.spec?.ports),
+ age: getAge(service.metadata?.creationTimestamp),
+ labels: service.metadata?.labels,
+ annotations: service.metadata?.annotations,
+ kind: s__('KubernetesDashboard|Service'),
+ };
+ }) || []
+ );
+ },
+ error(err) {
+ this.errorMessage = err?.message;
+ },
+ },
+ },
+ data() {
+ return {
+ k8sServices: [],
+ errorMessage: '',
+ };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.k8sServices.loading;
+ },
+ servicesStats() {
+ return [];
+ },
+ },
+ SERVICES_TABLE_FIELDS,
+};
+</script>
+<template>
+ <workload-layout
+ :loading="loading"
+ :error-message="errorMessage"
+ :stats="servicesStats"
+ :items="k8sServices"
+ :fields="$options.SERVICES_TABLE_FIELDS"
+ />
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/router/constants.js b/app/assets/javascripts/kubernetes_dashboard/router/constants.js
index a383ccd03e1..f02c01d7973 100644
--- a/app/assets/javascripts/kubernetes_dashboard/router/constants.js
+++ b/app/assets/javascripts/kubernetes_dashboard/router/constants.js
@@ -5,6 +5,7 @@ export const REPLICA_SETS_ROUTE_NAME = 'replicaSets';
export const DAEMON_SETS_ROUTE_NAME = 'daemonSets';
export const JOBS_ROUTE_NAME = 'jobs';
export const CRON_JOBS_ROUTE_NAME = 'cronJobs';
+export const SERVICES_ROUTE_NAME = 'services';
export const PODS_ROUTE_PATH = '/pods';
export const DEPLOYMENTS_ROUTE_PATH = '/deployments';
@@ -13,3 +14,4 @@ export const REPLICA_SETS_ROUTE_PATH = '/replicasets';
export const DAEMON_SETS_ROUTE_PATH = '/daemonsets';
export const JOBS_ROUTE_PATH = '/jobs';
export const CRON_JOBS_ROUTE_PATH = '/cronjobs';
+export const SERVICES_ROUTE_PATH = '/services';
diff --git a/app/assets/javascripts/kubernetes_dashboard/router/routes.js b/app/assets/javascripts/kubernetes_dashboard/router/routes.js
index 01bb48e8dce..7448508de8a 100644
--- a/app/assets/javascripts/kubernetes_dashboard/router/routes.js
+++ b/app/assets/javascripts/kubernetes_dashboard/router/routes.js
@@ -6,6 +6,7 @@ import ReplicaSetsPage from '../pages/replica_sets_page.vue';
import DaemonSetsPage from '../pages/daemon_sets_page.vue';
import JobsPage from '../pages/jobs_page.vue';
import CronJobsPage from '../pages/cron_jobs_page.vue';
+import ServicesPage from '../pages/services_page.vue';
import {
PODS_ROUTE_NAME,
@@ -22,6 +23,8 @@ import {
JOBS_ROUTE_PATH,
CRON_JOBS_ROUTE_NAME,
CRON_JOBS_ROUTE_PATH,
+ SERVICES_ROUTE_NAME,
+ SERVICES_ROUTE_PATH,
} from './constants';
export default [
@@ -81,4 +84,12 @@ export default [
title: s__('KubernetesDashboard|CronJobs'),
},
},
+ {
+ name: SERVICES_ROUTE_NAME,
+ path: SERVICES_ROUTE_PATH,
+ component: ServicesPage,
+ meta: {
+ title: s__('KubernetesDashboard|Services'),
+ },
+ },
];
diff --git a/app/assets/javascripts/vue_shared/components/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
index 1769a283d8c..0e8ecc36f37 100644
--- a/app/assets/javascripts/vue_shared/components/gl_countdown.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
@@ -30,6 +30,11 @@ export default {
mounted() {
const updateRemainingTime = () => {
const remainingMilliseconds = calculateRemainingMilliseconds(this.endDateString);
+
+ if (remainingMilliseconds < 1) {
+ this.$emit('timer-expired');
+ }
+
this.remainingTime = formatTime(remainingMilliseconds);
};
diff --git a/app/graphql/types/ci/ci_cd_setting_type.rb b/app/graphql/types/ci/ci_cd_setting_type.rb
index f01c63d717b..0c2d1b788af 100644
--- a/app/graphql/types/ci/ci_cd_setting_type.rb
+++ b/app/graphql/types/ci/ci_cd_setting_type.rb
@@ -27,7 +27,7 @@ module Types
field :merge_pipelines_enabled,
GraphQL::Types::Boolean,
null: true,
- description: 'Whether merge pipelines are enabled.',
+ description: 'Whether merged results pipelines are enabled.',
method: :merge_pipelines_enabled?
field :project,
Types::ProjectType,
diff --git a/app/models/member.rb b/app/models/member.rb
index 845a711e986..d3101656739 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -288,6 +288,14 @@ class Member < ApplicationRecord
refresh_member_authorized_projects
end
+ after_create if: :update_organization_user? do
+ Organizations::OrganizationUser.upsert(
+ { organization_id: source.organization_id, user_id: user_id, access_level: :default },
+ unique_by: [:organization_id, :user_id],
+ on_duplicate: :skip # Do not change access_level, could make :owner :default
+ )
+ end
+
attribute :notification_level, default: -> { NotificationSetting.levels[:global] }
class << self
@@ -657,6 +665,12 @@ class Member < ApplicationRecord
user&.project_bot?
end
+ def update_organization_user?
+ return false unless Feature.enabled?(:update_organization_users, source.root_ancestor, type: :gitlab_com_derisk)
+
+ !invite? && source.organization.present?
+ end
+
def log_invitation_token_cleanup
return true unless Gitlab.com? && invite? && invite_accepted_at?
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
index f6521eada40..ffb8d3a95a2 100644
--- a/app/models/users/phone_number_validation.rb
+++ b/app/models/users/phone_number_validation.rb
@@ -4,6 +4,11 @@ module Users
class PhoneNumberValidation < ApplicationRecord
include IgnorableColumns
+ # SMS send attempts subsequent to the first one will have wait times of 1
+ # min, 3 min, 5 min after each one respectively. Wait time between the fifth
+ # attempt and so on will be 10 minutes.
+ SMS_SEND_WAIT_TIMES = [1.minute, 3.minutes, 5.minutes, 10.minutes].freeze
+
self.primary_key = :user_id
self.table_name = 'user_phone_number_validations'
@@ -62,5 +67,18 @@ module Users
def validated?
validated_at.present?
end
+
+ def sms_send_allowed_after
+ return unless Feature.enabled?(:sms_send_wait_time, user)
+
+ # first send is allowed anytime
+ return if sms_send_count < 1
+ return unless sms_sent_at
+
+ max_wait_time = SMS_SEND_WAIT_TIMES.last
+ wait_time = SMS_SEND_WAIT_TIMES.fetch(sms_send_count - 1, max_wait_time)
+
+ sms_sent_at + wait_time
+ end
end
end
diff --git a/app/policies/organizations/organization_policy.rb b/app/policies/organizations/organization_policy.rb
index afd8c6e144f..a203a58b164 100644
--- a/app/policies/organizations/organization_policy.rb
+++ b/app/policies/organizations/organization_policy.rb
@@ -3,6 +3,7 @@
module Organizations
class OrganizationPolicy < BasePolicy
condition(:organization_user) { @subject.user?(@user) }
+ condition(:organization_owner) { @subject.owner?(@user) }
desc 'Organization is public'
condition(:public_organization, scope: :subject, score: 0) { true }
@@ -18,11 +19,14 @@ module Organizations
enable :read_organization_user
end
- rule { organization_user }.policy do
+ rule { organization_owner }.policy do
enable :admin_organization
- enable :create_group
+ end
+
+ rule { organization_user }.policy do
enable :read_organization
enable :read_organization_user
+ enable :create_group
end
end
end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index fc0695c7f62..dfad9f7f673 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -345,6 +345,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: cronjob:click_house_event_authors_consistency_cron
+ :worker_name: ClickHouse::EventAuthorsConsistencyCronWorker
+ :feature_category: :value_stream_management
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:click_house_events_sync
:worker_name: ClickHouse::EventsSyncWorker
:feature_category: :value_stream_management
diff --git a/app/workers/click_house/event_authors_consistency_cron_worker.rb b/app/workers/click_house/event_authors_consistency_cron_worker.rb
new file mode 100644
index 00000000000..5c52cda0204
--- /dev/null
+++ b/app/workers/click_house/event_authors_consistency_cron_worker.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+module ClickHouse
+ # rubocop: disable CodeReuse/ActiveRecord -- Building worker-specific ActiveRecord and ClickHouse queries
+ class EventAuthorsConsistencyCronWorker
+ include ApplicationWorker
+ include ClickHouseWorker
+ include Gitlab::ExclusiveLeaseHelpers
+ include Gitlab::Utils::StrongMemoize
+
+ idempotent!
+ queue_namespace :cronjob
+ data_consistency :delayed
+ worker_has_external_dependencies! # the worker interacts with a ClickHouse database
+ feature_category :value_stream_management
+
+ MAX_TTL = 5.minutes.to_i
+ MAX_RUNTIME = 150.seconds
+ MAX_AUTHOR_DELETIONS = 2000
+ CLICK_HOUSE_BATCH_SIZE = 100_000
+ POSTGRESQL_BATCH_SIZE = 2500
+
+ def perform
+ return unless enabled?
+
+ runtime_limiter = Analytics::CycleAnalytics::RuntimeLimiter.new(MAX_RUNTIME)
+
+ in_lock(self.class.to_s, ttl: MAX_TTL, retries: 0) do
+ author_records_to_delete = []
+ last_processed_id = 0
+ iterator.each_batch(column: :author_id, of: CLICK_HOUSE_BATCH_SIZE) do |scope|
+ query = scope.select(Arel.sql('DISTINCT author_id')).to_sql
+ ids_from_click_house = connection.select(query).pluck('author_id').sort
+
+ ids_from_click_house.each_slice(POSTGRESQL_BATCH_SIZE) do |ids|
+ author_records_to_delete.concat(missing_user_ids(ids))
+ last_processed_id = ids.last
+
+ to_be_deleted_size = author_records_to_delete.size
+ if to_be_deleted_size >= MAX_AUTHOR_DELETIONS
+ metadata.merge!(status: :deletion_limit_reached, deletions: to_be_deleted_size)
+ break
+ end
+
+ if runtime_limiter.over_time?
+ metadata.merge!(status: :over_time, deletions: to_be_deleted_size)
+ break
+ end
+ end
+
+ break if limit_was_reached?
+ end
+
+ delete_records_from_click_house(author_records_to_delete)
+
+ last_processed_id = 0 if table_fully_processed?
+ ClickHouse::SyncCursor.update_cursor_for(:event_authors_consistency_check, last_processed_id)
+
+ log_extra_metadata_on_done(:result, metadata)
+ end
+ end
+
+ private
+
+ def metadata
+ @metadata ||= { status: :processed, deletions: 0 }
+ end
+
+ def limit_was_reached?
+ metadata[:status] == :deletion_limit_reached || metadata[:status] == :over_time
+ end
+
+ def table_fully_processed?
+ metadata[:status] == :processed
+ end
+
+ def enabled?
+ ClickHouse::Client.database_configured?(:main) && Feature.enabled?(:event_sync_worker_for_click_house)
+ end
+
+ def previous_author_id
+ value = ClickHouse::SyncCursor.cursor_for(:event_authors_consistency_check)
+ value == 0 ? nil : value
+ end
+ strong_memoize_attr :previous_author_id
+
+ def iterator
+ builder = ClickHouse::QueryBuilder.new('event_authors')
+ ClickHouse::Iterator.new(query_builder: builder, connection: connection, min_value: previous_author_id)
+ end
+
+ def connection
+ @connection ||= ClickHouse::Connection.new(:main)
+ end
+
+ def missing_user_ids(ids)
+ value_list = Arel::Nodes::ValuesList.new(ids.map { |id| [id] })
+ User
+ .from("(#{value_list.to_sql}) AS user_ids(id)")
+ .where('NOT EXISTS (SELECT 1 FROM users WHERE id = user_ids.id)')
+ .pluck(:id)
+ end
+
+ def delete_records_from_click_house(ids)
+ query = ClickHouse::Client::Query.new(
+ raw_query: "DELETE FROM events WHERE author_id IN ({author_ids:Array(UInt64)})",
+ placeholders: { author_ids: ids.to_json }
+ )
+
+ connection.execute(query)
+
+ query = ClickHouse::Client::Query.new(
+ raw_query: "DELETE FROM event_authors WHERE author_id IN ({author_ids:Array(UInt64)})",
+ placeholders: { author_ids: ids.to_json }
+ )
+
+ connection.execute(query)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
diff --git a/config/feature_flags/development/sms_send_wait_time.yml b/config/feature_flags/development/sms_send_wait_time.yml
new file mode 100644
index 00000000000..e4cde3477ca
--- /dev/null
+++ b/config/feature_flags/development/sms_send_wait_time.yml
@@ -0,0 +1,8 @@
+---
+name: sms_send_wait_time
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137850
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432975
+milestone: '16.8'
+type: development
+group: group::anti-abuse
+default_enabled: false
diff --git a/config/feature_flags/gitlab_com_derisk/update_organization_users.yml b/config/feature_flags/gitlab_com_derisk/update_organization_users.yml
new file mode 100644
index 00000000000..09074850150
--- /dev/null
+++ b/config/feature_flags/gitlab_com_derisk/update_organization_users.yml
@@ -0,0 +1,9 @@
+---
+name: update_organization_users
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419366
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139188
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/435868
+milestone: '16.8'
+group: group::tenant scale
+type: gitlab_com_derisk
+default_enabled: false
diff --git a/config/feature_flags/ops/ai_duo_chat_switch.yml b/config/feature_flags/ops/ai_duo_chat_switch.yml
new file mode 100644
index 00000000000..cd3fe3ab937
--- /dev/null
+++ b/config/feature_flags/ops/ai_duo_chat_switch.yml
@@ -0,0 +1,9 @@
+---
+name: ai_duo_chat_switch
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/434802
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140352
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17301
+milestone: '16.8'
+group: group::ai framework
+type: ops
+default_enabled: true
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index cd2bfd85d0a..4b279db68b8 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -900,6 +900,9 @@ Gitlab.ee do
Settings.cron_jobs['click_house_events_sync_worker'] ||= {}
Settings.cron_jobs['click_house_events_sync_worker']['cron'] ||= "*/3 * * * *"
Settings.cron_jobs['click_house_events_sync_worker']['job_class'] = 'ClickHouse::EventsSyncWorker'
+ Settings.cron_jobs['click_house_event_authors_consistency_cron_worker'] ||= {}
+ Settings.cron_jobs['click_house_event_authors_consistency_cron_worker']['cron'] ||= "*/30 * * * *"
+ Settings.cron_jobs['click_house_event_authors_consistency_cron_worker']['job_class'] = 'ClickHouse::EventAuthorsConsistencyCronWorker'
Settings.cron_jobs['vertex_ai_refresh_access_token_worker'] ||= {}
Settings.cron_jobs['vertex_ai_refresh_access_token_worker']['cron'] ||= '*/50 * * * *'
Settings.cron_jobs['vertex_ai_refresh_access_token_worker']['job_class'] = 'Llm::VertexAiAccessTokenRefreshWorker'
diff --git a/config/initializers/macos.rb b/config/initializers/macos.rb
index 860167e12cd..7ec022add18 100644
--- a/config/initializers/macos.rb
+++ b/config/initializers/macos.rb
@@ -28,4 +28,12 @@ if RUBY_PLATFORM.include?('darwin')
time_zone_name = CFTimeZone.CFTimeZoneGetName(default_time_zone)
CFTimeZone.CFRelease(time_zone_name)
CFTimeZone.CFRelease(default_time_zone)
+
+ # With curl v8.2.0, the thread unsafe macOS API call to
+ # SCDynamicStoreCopyProxies has been moved to the global init function
+ # (https://github.com/curl/curl/issues/11252). The Elasticsearch
+ # gem uses Typhoeus, which uses Ethon to wrap libcurl.
+ # Init curl to ensure Spring works
+ # (https://github.com/elastic/elasticsearch-ruby/issues/2244).
+ Ethon::Curl.init
end
diff --git a/db/migrate/20231124022520_add_sms_sent_at_and_sms_send_count_to_phone_number_validations.rb b/db/migrate/20231124022520_add_sms_sent_at_and_sms_send_count_to_phone_number_validations.rb
new file mode 100644
index 00000000000..40508d8da6e
--- /dev/null
+++ b/db/migrate/20231124022520_add_sms_sent_at_and_sms_send_count_to_phone_number_validations.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddSmsSentAtAndSmsSendCountToPhoneNumberValidations < Gitlab::Database::Migration[2.2]
+ milestone '16.8'
+ enable_lock_retries!
+
+ def up
+ add_column :user_phone_number_validations, :sms_sent_at, :datetime_with_timezone, null: true
+ add_column :user_phone_number_validations, :sms_send_count, :smallint, default: 0, null: false
+ end
+
+ def down
+ remove_column :user_phone_number_validations, :sms_sent_at, if_exists: true
+ remove_column :user_phone_number_validations, :sms_send_count, if_exists: true
+ end
+end
diff --git a/db/migrate/20240101031938_add_admin_terraform_state_to_member_roles.rb b/db/migrate/20240101031938_add_admin_terraform_state_to_member_roles.rb
new file mode 100644
index 00000000000..89222664d01
--- /dev/null
+++ b/db/migrate/20240101031938_add_admin_terraform_state_to_member_roles.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddAdminTerraformStateToMemberRoles < Gitlab::Database::Migration[2.2]
+ milestone '16.8'
+ enable_lock_retries!
+
+ def change
+ add_column :member_roles, :admin_terraform_state, :boolean, default: false, null: false
+ end
+end
diff --git a/db/post_migrate/20240109025151_create_index_on_id_convert_to_bigint_for_system_note_metadata_async.rb b/db/post_migrate/20240109025151_create_index_on_id_convert_to_bigint_for_system_note_metadata_async.rb
new file mode 100644
index 00000000000..281704a1620
--- /dev/null
+++ b/db/post_migrate/20240109025151_create_index_on_id_convert_to_bigint_for_system_note_metadata_async.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CreateIndexOnIdConvertToBigintForSystemNoteMetadataAsync < Gitlab::Database::Migration[2.2]
+ milestone '16.8'
+
+ TABLE_NAME = :system_note_metadata
+ INDEX_NAME = 'index_system_note_metadata_pkey_on_id_convert_to_bigint'
+
+ def up
+ prepare_async_index TABLE_NAME, :id_convert_to_bigint, unique: true, name: INDEX_NAME
+ end
+
+ def down
+ unprepare_async_index TABLE_NAME, :id_convert_to_bigint, unique: true, name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20231124022520 b/db/schema_migrations/20231124022520
new file mode 100644
index 00000000000..408c6f976f8
--- /dev/null
+++ b/db/schema_migrations/20231124022520
@@ -0,0 +1 @@
+bfa32c41d867fa4de24ac0a81d1f99f14e868b2c5bd453f799e1a3b3eebd1d51 \ No newline at end of file
diff --git a/db/schema_migrations/20240101031938 b/db/schema_migrations/20240101031938
new file mode 100644
index 00000000000..5b9395a568f
--- /dev/null
+++ b/db/schema_migrations/20240101031938
@@ -0,0 +1 @@
+d0cb92dc098f069e02d457f7c497dc24f544f6a27a8426dcd3446ad16bd9cc44 \ No newline at end of file
diff --git a/db/schema_migrations/20240109025151 b/db/schema_migrations/20240109025151
new file mode 100644
index 00000000000..d6d47f823e2
--- /dev/null
+++ b/db/schema_migrations/20240109025151
@@ -0,0 +1 @@
+b40f751b4b06dd94de38e3fa260e07e56359828ca1ae1799ca4d65bd873fa8af \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 55d358c8976..0a048082a93 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -18948,6 +18948,7 @@ CREATE TABLE member_roles (
archive_project boolean DEFAULT false NOT NULL,
manage_group_access_tokens boolean DEFAULT false NOT NULL,
remove_project boolean DEFAULT false NOT NULL,
+ admin_terraform_state boolean DEFAULT false NOT NULL,
CONSTRAINT check_4364846f58 CHECK ((char_length(description) <= 255)),
CONSTRAINT check_9907916995 CHECK ((char_length(name) <= 255))
);
@@ -24978,6 +24979,8 @@ CREATE TABLE user_phone_number_validations (
country text NOT NULL,
phone_number text NOT NULL,
telesign_reference_xid text,
+ sms_sent_at timestamp with time zone,
+ sms_send_count smallint DEFAULT 0 NOT NULL,
CONSTRAINT check_193736da9f CHECK ((char_length(country) <= 3)),
CONSTRAINT check_d2f31fc815 CHECK ((char_length(phone_number) <= 12)),
CONSTRAINT check_d7af4d3eb5 CHECK ((char_length(telesign_reference_xid) <= 255))
diff --git a/doc/administration/admin_area.md b/doc/administration/admin_area.md
index ff18ac8ff3a..f9b26cd364d 100644
--- a/doc/administration/admin_area.md
+++ b/doc/administration/admin_area.md
@@ -470,3 +470,12 @@ The content of each log file is listed in chronological order. To minimize perfo
### Audit Events **(PREMIUM SELF)**
The **Audit Events** page lists changes made within the GitLab server. With this information you can control, analyze, and track every change.
+
+### Statistics
+
+The **Instance overview** section of the Dashboard lists the current statistics of the GitLab instance. This information is retrieved using the [Application statistics API](../api/statistics.md#get-current-application-statistics).
+
+NOTE:
+These statistics show exact counts for values less than 10,000. For values of 10,000 and higher, these statistics show approximate data
+when [TablesampleCountStrategy](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/count/tablesample_count_strategy.rb?ref_type=heads#L16) and [ReltuplesCountStrategy](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/count/reltuples_count_strategy.rb?ref_type=heads) strategies are used for calculations.
+.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 45473c2a931..fed954b7199 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -6261,7 +6261,7 @@ Input type: `ProjectCiCdSettingsUpdateInput`
| <a id="mutationprojectcicdsettingsupdateinboundjobtokenscopeenabled"></a>`inboundJobTokenScopeEnabled` | [`Boolean`](#boolean) | Indicates CI/CD job tokens generated in other projects have restricted access to this project. |
| <a id="mutationprojectcicdsettingsupdatejobtokenscopeenabled"></a>`jobTokenScopeEnabled` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated:** Outbound job token scope is being removed. This field can now only be set to false. Deprecated in 16.0. |
| <a id="mutationprojectcicdsettingsupdatekeeplatestartifact"></a>`keepLatestArtifact` | [`Boolean`](#boolean) | Indicates if the latest artifact should be kept for the project. |
-| <a id="mutationprojectcicdsettingsupdatemergepipelinesenabled"></a>`mergePipelinesEnabled` | [`Boolean`](#boolean) | Indicates if merge pipelines are enabled for the project. |
+| <a id="mutationprojectcicdsettingsupdatemergepipelinesenabled"></a>`mergePipelinesEnabled` | [`Boolean`](#boolean) | Indicates if merged results pipelines are enabled for the project. |
| <a id="mutationprojectcicdsettingsupdatemergetrainsenabled"></a>`mergeTrainsEnabled` | [`Boolean`](#boolean) | Indicates if merge trains are enabled for the project. |
| <a id="mutationprojectcicdsettingsupdatemergetrainsskiptrainallowed"></a>`mergeTrainsSkipTrainAllowed` | [`Boolean`](#boolean) | Indicates whether an option is allowed to merge without refreshing the merge train. Ignored unless the `merge_trains_skip_train` feature flag is also enabled. |
@@ -6800,7 +6800,7 @@ Input type: `RunnersExportUsageInput`
| ---- | ---- | ----------- |
| <a id="mutationrunnersexportusageclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationrunnersexportusagefromdate"></a>`fromDate` | [`ISO8601Date`](#iso8601date) | UTC start date of the period to report on. Defaults to the start of last full month. |
-| <a id="mutationrunnersexportusagemaxprojectcount"></a>`maxProjectCount` | [`Int`](#int) | Maximum number of projects to return. All other runner usage will be attributed to a '<Other projects>' entry. Defaults to 1000 projects. |
+| <a id="mutationrunnersexportusagemaxprojectcount"></a>`maxProjectCount` | [`Int`](#int) | Maximum number of projects to return. All other runner usage will be attributed to an `<Other projects>` entry. Defaults to 1000 projects. |
| <a id="mutationrunnersexportusagetodate"></a>`toDate` | [`ISO8601Date`](#iso8601date) | UTC end date of the period to report on. " \ "Defaults to the end of the month specified by `fromDate`. |
| <a id="mutationrunnersexportusagetype"></a>`type` | [`CiRunnerType`](#cirunnertype) | Scope of the runners to include in the report. |
@@ -25854,7 +25854,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectcicdsettinginboundjobtokenscopeenabled"></a>`inboundJobTokenScopeEnabled` | [`Boolean`](#boolean) | Indicates CI/CD job tokens generated in other projects have restricted access to this project. |
| <a id="projectcicdsettingjobtokenscopeenabled"></a>`jobTokenScopeEnabled` | [`Boolean`](#boolean) | Indicates CI/CD job tokens generated in this project have restricted access to other projects. |
| <a id="projectcicdsettingkeeplatestartifact"></a>`keepLatestArtifact` | [`Boolean`](#boolean) | Whether to keep the latest builds artifacts. |
-| <a id="projectcicdsettingmergepipelinesenabled"></a>`mergePipelinesEnabled` | [`Boolean`](#boolean) | Whether merge pipelines are enabled. |
+| <a id="projectcicdsettingmergepipelinesenabled"></a>`mergePipelinesEnabled` | [`Boolean`](#boolean) | Whether merged results pipelines are enabled. |
| <a id="projectcicdsettingmergetrainsenabled"></a>`mergeTrainsEnabled` | [`Boolean`](#boolean) | Whether merge trains are enabled. |
| <a id="projectcicdsettingmergetrainsskiptrainallowed"></a>`mergeTrainsSkipTrainAllowed` | [`Boolean!`](#boolean) | Whether merge immediately is allowed for merge trains. |
| <a id="projectcicdsettingproject"></a>`project` | [`Project`](#project) | Project the CI/CD settings belong to. |
@@ -30859,6 +30859,7 @@ Member role permission.
| ----- | ----------- |
| <a id="memberrolepermissionadmin_group_member"></a>`ADMIN_GROUP_MEMBER` | Allows to admin group members. |
| <a id="memberrolepermissionadmin_merge_request"></a>`ADMIN_MERGE_REQUEST` | Allows to approve merge requests. |
+| <a id="memberrolepermissionadmin_terraform_state"></a>`ADMIN_TERRAFORM_STATE` | Allows to admin terraform state. |
| <a id="memberrolepermissionadmin_vulnerability"></a>`ADMIN_VULNERABILITY` | Allows admin access to the vulnerability reports. |
| <a id="memberrolepermissionarchive_project"></a>`ARCHIVE_PROJECT` | Allows to archive projects. |
| <a id="memberrolepermissionmanage_group_access_tokens"></a>`MANAGE_GROUP_ACCESS_TOKENS` | Allows manage access to the group access tokens. |
diff --git a/doc/api/member_roles.md b/doc/api/member_roles.md
index 2fd10d99fda..2bfbc29081f 100644
--- a/doc/api/member_roles.md
+++ b/doc/api/member_roles.md
@@ -19,6 +19,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Archive project introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134998) in GitLab 16.7.
> - [Delete project introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139696) in GitLab 16.8.
> - [Manage group access tokens introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140115) in GitLab 16.8.
+> - [Admin terraform state introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140759) in GitLab 16.8.
FLAG:
On self-managed GitLab, by default these features are not available. To make them available, an administrator can [enable the feature flags](../administration/feature_flags.md) named `admin_group_member` and `manage_project_access_tokens`.
@@ -46,6 +47,7 @@ If successful, returns [`200`](rest/index.md#status-codes) and the following res
| `[].group_id` | integer | The ID of the group that the member role belongs to. |
| `[].base_access_level` | integer | Base access level for member role. Valid values are 10 (Guest), 20 (Reporter), 30 (Developer), 40 (Maintainer), or 50 (Owner).|
| `[].admin_merge_request` | boolean | Permission to admin project merge requests and enables the ability to `download_code`. |
+| `[].admin_terraform_state` | boolean | Permission to admin project terraform state. |
| `[].admin_vulnerability` | boolean | Permission to admin project vulnerabilities. |
| `[].read_code` | boolean | Permission to read project code. |
| `[].read_dependency` | boolean | Permission to read project dependencies. |
@@ -73,6 +75,7 @@ Example response:
"group_id": 84,
"base_access_level": 10,
"admin_merge_request": false,
+ "admin_terraform_state": false,
"admin_vulnerability": false,
"read_code": true,
"read_dependency": false,
@@ -88,8 +91,9 @@ Example response:
"description: "Custom guest that read and admin security entities",
"group_id": 84,
"base_access_level": 10,
- "admin_merge_request": false,
"admin_vulnerability": true,
+ "admin_merge_request": false,
+ "admin_terraform_state": false,
"read_code": false,
"read_dependency": true,
"read_vulnerability": true,
@@ -120,6 +124,7 @@ To add a member role to a group, the group must be at root-level (have no parent
| `description` | string | no | The description of the member role. |
| `base_access_level` | integer | yes | Base access level for configured role. Valid values are 10 (Guest), 20 (Reporter), 30 (Developer), 40 (Maintainer), or 50 (Owner).|
| `admin_merge_request` | boolean | no | Permission to admin project merge requests. |
+| `admin_terraform_state` | boolean | no | Permission to admin project terraform state. |
| `admin_vulnerability` | boolean | no | Permission to admin project vulnerabilities. |
| `read_code` | boolean | no | Permission to read project code. |
| `read_dependency` | boolean | no | Permission to read project dependencies. |
@@ -135,6 +140,7 @@ If successful, returns [`201`](rest/index.md#status-codes) and the following att
| `group_id` | integer | The ID of the group that the member role belongs to. |
| `base_access_level` | integer | Base access level for member role. |
| `admin_merge_request` | boolean | Permission to admin project merge requests. |
+| `admin_terraform_state` | boolean | Permission to admin project terraform state. |
| `admin_vulnerability` | boolean | Permission to admin project vulnerabilities. |
| `read_code` | boolean | Permission to read project code. |
| `read_dependency` | boolean | Permission to read project dependencies. |
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 33f05357876..257dd88a770 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1538,7 +1538,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your-token>" \
| `jobs_enabled` | boolean | No | _(Deprecated)_ Enable jobs for this project. Use `builds_access_level` instead. |
| `lfs_enabled` | boolean | No | Enable LFS. |
| `merge_method` | string | No | Set the [merge method](#project-merge-method) used. |
-| `merge_pipelines_enabled` | boolean | No | Enable or disable merge pipelines. |
+| `merge_pipelines_enabled` | boolean | No | Enable or disable merged results pipelines. |
| `merge_requests_access_level` | string | No | One of `disabled`, `private`, or `enabled`. |
| `merge_requests_enabled` | boolean | No | _(Deprecated)_ Enable merge requests for this project. Use `merge_requests_access_level` instead. |
| `merge_trains_enabled` | boolean | No | Enable or disable merge trains. |
@@ -1739,7 +1739,7 @@ Supported attributes:
| `lfs_enabled` | boolean | No | Enable LFS. |
| `merge_commit_template` | string | No | [Template](../user/project/merge_requests/commit_templates.md) used to create merge commit message in merge requests. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20263) in GitLab 14.5.)_ |
| `merge_method` | string | No | Set the [merge method](#project-merge-method) used. |
-| `merge_pipelines_enabled` | boolean | No | Enable or disable merge pipelines. |
+| `merge_pipelines_enabled` | boolean | No | Enable or disable merged results pipelines. |
| `merge_requests_access_level` | string | No | One of `disabled`, `private`, or `enabled`. |
| `merge_requests_enabled` | boolean | No | _(Deprecated)_ Enable merge requests for this project. Use `merge_requests_access_level` instead. |
| `merge_requests_template` **(PREMIUM ALL)** | string | No | Default description for merge requests. Description is parsed with GitLab Flavored Markdown. See [Templates for issues and merge requests](#templates-for-issues-and-merge-requests). |
diff --git a/doc/api/statistics.md b/doc/api/statistics.md
index 8868f6d5190..fc0aa9a0b39 100644
--- a/doc/api/statistics.md
+++ b/doc/api/statistics.md
@@ -12,7 +12,8 @@ List the current statistics of the GitLab instance. You have to be an
administrator to perform this action.
NOTE:
-These statistics are approximate.
+These statistics show exact counts for values less than 10,000. For values of 10,000 and higher, these statistics show approximate data
+when [TablesampleCountStrategy](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/count/tablesample_count_strategy.rb?ref_type=heads#L16) and [ReltuplesCountStrategy](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/count/reltuples_count_strategy.rb?ref_type=heads) strategies are used for calculations.
```plaintext
GET /application/statistics
diff --git a/doc/development/fe_guide/frontend_faq.md b/doc/development/fe_guide/frontend_faq.md
index b8e98b47cac..0ab64841365 100644
--- a/doc/development/fe_guide/frontend_faq.md
+++ b/doc/development/fe_guide/frontend_faq.md
@@ -202,3 +202,10 @@ To see what polyfills are being used:
### 9. Why is my page broken in dark mode?
See [dark mode docs](dark_mode.md)
+
+### 10. How to render GitLab-flavored Markdown?
+
+If you need to render [GitLab-flavored Markdown](../gitlab_flavored_markdown/index.md), then there are two things that you require:
+
+- Pass the GLFM content with the `v-safe-html` directive to a `div` HTML element inside your Vue component
+- Add the `md` class to the root div, which will apply the appropriate CSS styling
diff --git a/doc/development/testing_guide/end_to_end/execution_context_selection.md b/doc/development/testing_guide/end_to_end/execution_context_selection.md
index f625dc466b9..f940cf289d9 100644
--- a/doc/development/testing_guide/end_to_end/execution_context_selection.md
+++ b/doc/development/testing_guide/end_to_end/execution_context_selection.md
@@ -48,6 +48,7 @@ Matches use:
| The `nightly` and `canary` pipelines | `only: { pipeline: [:nightly, :canary] }` | ["nightly scheduled pipeline"](https://gitlab.com/gitlab-org/gitlab/-/pipeline_schedules) and ["canary"](https://gitlab.com/gitlab-org/quality/canary) |
| The `ee:instance` job | `only: { job: 'ee:instance' }` | The `ee:instance` job in any pipeline |
| Any `quarantine` job | `only: { job: '.*quarantine' }` | Any job ending in `quarantine` in any pipeline |
+| Local development environment | `only: :local` | Any environment where `Runtime::Env.running_in_ci?` is false |
| Any run where condition evaluates to a truthy value | `only: { condition: -> { ENV['TEST_ENV'] == 'true' } }` | Any run where `TEST_ENV` is set to true
```ruby
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 6098a061917..0f18a1252d0 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -894,17 +894,15 @@ you can use the `MAVEN_CLI_OPTS` CI/CD variable.
Read more on [how to use private Maven repositories](../index.md#using-private-maven-repositories).
-#### FIPS-enabled images
+### FIPS-enabled images
-> Introduced in GitLab 14.10. GitLab team members can view more information in this confidential issue: `https://gitlab.com/gitlab-org/gitlab/-/issues/354796`
+> - Introduced in GitLab 14.10. GitLab team members can view more information in this confidential issue: `https://gitlab.com/gitlab-org/gitlab/-/issues/354796`
+> - Introduced in GitLab 15.0 - Gemnasium uses FIPS-enabled images when FIPS mode is enabled.
GitLab also offers [FIPS-enabled Red Hat UBI](https://www.redhat.com/en/blog/introducing-red-hat-universal-base-image)
-versions of the Gemnasium images. You can therefore replace standard images with FIPS-enabled images.
-
-Gemnasium scanning jobs automatically use FIPS-enabled image when FIPS mode is enabled in the GitLab instance.
-([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/357922) in GitLab 15.0.)
-
-To manually switch to FIPS-enabled images, set the variable `DS_IMAGE_SUFFIX` to `"-fips"`.
+versions of the Gemnasium images. When FIPS mode is enabled in the GitLab instance, Gemnasium
+scanning jobs automatically use the FIPS-enabled images. To manually switch to FIPS-enabled images,
+set the variable `DS_IMAGE_SUFFIX` to `"-fips"`.
Dependency scanning for Gradle projects and auto-remediation for Yarn projects are not supported in FIPS mode.
diff --git a/doc/user/project/pages/redirects.md b/doc/user/project/pages/redirects.md
index d13f30060e7..2b33e7dc343 100644
--- a/doc/user/project/pages/redirects.md
+++ b/doc/user/project/pages/redirects.md
@@ -26,7 +26,7 @@ are supported.
| Rewrites (other than `200`) | **{dotted-circle}** No | `/en/* /en/404.html 404` |
| Query parameters | **{dotted-circle}** No | `/store id=:id /blog/:id 301` |
| Force ([shadowing](https://docs.netlify.com/routing/redirects/rewrites-proxies/#shadowing)) | **{dotted-circle}** No | `/app/ /app/index.html 200!` |
-| Domain-level redirects | **{dotted-circle}** No | `http://blog.example.com/* https://www.example.com/blog/:splat 301` |
+| [Domain-level redirects](#domain-level-redirects) | **{check-circle}** Yes | `http://blog.example.com/* https://www.example.com/blog/:splat 301` |
| Redirect by country or language | **{dotted-circle}** No | `/ /anz 302 Country=au,nz` |
| Redirect by role | **{dotted-circle}** No | `/admin/* 200! Role=admin` |
@@ -119,6 +119,30 @@ request matches the `from`:
This status code can be used in combination with [splat rules](#splats) to dynamically
rewrite the URL.
+## Domain-level redirects
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-pages/-/merge_requests/936) in GitLab 16.8 [with a flag](../../../administration/feature_flags.md) named `FF_ENABLE_DOMAIN_REDIRECT`. Disabled by default.
+
+To create a domain-level redirect, add a domain-level path (beginning with `http://`
+or `https://`) to either:
+
+- The `to` path only.
+- The `from` and `to` paths.
+The supported [HTTP status codes](#http-status-codes) are `301` and `302`:
+
+```plaintext
+# 301 permanent redirect
+http://blog.example.com/file_1.html https://www.example.com/blog/file_1.html 301
+/file_2.html https://www.example.com/blog/file_2.html 301
+
+# 302 temporary redirect
+http://blog.example.com/file_3.html https://www.example.com/blog/file_3.html 302
+/file_4.html https://www.example.com/blog/file_4.html 302
+```
+
+Domain-level redirects can be used in combination with [splat rules](#splats) (including splat placeholders)
+to dynamically rewrite the URL path.
+
## Splats
> [Introduced](https://gitlab.com/gitlab-org/gitlab-pages/-/merge_requests/458) in GitLab 14.3.
diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md
index 1e52aaaa211..d6bcae57322 100644
--- a/doc/user/project/repository/branches/index.md
+++ b/doc/user/project/repository/branches/index.md
@@ -301,6 +301,8 @@ When you create a merge request, the workflow checks the name of the branch. If
branch name matches the workflow, the merge request targets the branch you specify. If the branch name does not match, the merge request targets the
default branch of the project.
+Rules are processed on a "first-match" basis - if two rules match the same branch name, the top-most rule is applied.
+
Prerequisites:
- You must have at least the Maintainer role.
diff --git a/lib/click_house/iterator.rb b/lib/click_house/iterator.rb
index 4bfbc624dc7..f17f3efa8a5 100644
--- a/lib/click_house/iterator.rb
+++ b/lib/click_house/iterator.rb
@@ -22,9 +22,10 @@ module ClickHouse
# builder = ClickHouse::QueryBuilder.new('event_authors').where(type: 'some_type')
class Iterator
# rubocop: disable CodeReuse/ActiveRecord -- this is a ClickHouse query builder class usin Arel
- def initialize(query_builder:, connection:)
+ def initialize(query_builder:, connection:, min_value: nil)
@query_builder = query_builder
@connection = connection
+ @min_value = min_value
end
def each_batch(column: :id, of: 10_000)
@@ -36,18 +37,18 @@ module ClickHouse
row = connection.select(min_max_query.to_sql).first
return if row.nil?
- min_value = row['min']
- max_value = row['max']
- return if max_value == 0
+ min = min_value || row['min']
+ max = row['max']
+ return if max == 0
loop do
- break if min_value > max_value
+ break if min > max
yield query_builder
- .where(table[column].gteq(min_value))
- .where(table[column].lt(min_value + of))
+ .where(table[column].gteq(min))
+ .where(table[column].lt(min + of))
- min_value += of
+ min += of
end
end
@@ -55,7 +56,7 @@ module ClickHouse
delegate :table, to: :query_builder
- attr_reader :query_builder, :connection
+ attr_reader :query_builder, :connection, :min_value
# rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/lib/gitlab/ci/parsers/security/common.rb b/lib/gitlab/ci/parsers/security/common.rb
index c46d719e37d..ede0f62ea51 100644
--- a/lib/gitlab/ci/parsers/security/common.rb
+++ b/lib/gitlab/ci/parsers/security/common.rb
@@ -132,7 +132,6 @@ module Gitlab
uuid: uuid,
report_type: report.type,
name: finding_name(data, identifiers, location),
- compare_key: data['cve'] || '',
location: location,
evidence: evidence,
severity: ::Enums::Vulnerability.parse_severity_level(data['severity']),
diff --git a/lib/gitlab/ci/reports/security/finding.rb b/lib/gitlab/ci/reports/security/finding.rb
index fa8494483d3..fbca1e674d1 100644
--- a/lib/gitlab/ci/reports/security/finding.rb
+++ b/lib/gitlab/ci/reports/security/finding.rb
@@ -7,7 +7,6 @@ module Gitlab
class Finding
include ::VulnerabilityFindingHelpers
- attr_reader :compare_key
attr_reader :confidence
attr_reader :identifiers
attr_reader :flags
@@ -34,10 +33,7 @@ module Gitlab
delegate :file_path, :start_line, :end_line, to: :location
- alias_method :cve, :compare_key
-
- def initialize(compare_key:, identifiers:, flags: [], links: [], remediations: [], location:, evidence:, metadata_version:, name:, original_data:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: [], project_id: nil, vulnerability_finding_signatures_enabled: false, found_by_pipeline: nil, cvss: []) # rubocop:disable Metrics/ParameterLists
- @compare_key = compare_key
+ def initialize(identifiers:, flags: [], links: [], remediations: [], location:, evidence:, metadata_version:, name:, original_data:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: [], project_id: nil, vulnerability_finding_signatures_enabled: false, found_by_pipeline: nil, cvss: []) # rubocop:disable Metrics/ParameterLists
@confidence = confidence
@identifiers = identifiers
@flags = flags
@@ -65,7 +61,6 @@ module Gitlab
def to_hash
%i[
- compare_key
confidence
identifiers
flags
@@ -84,7 +79,6 @@ module Gitlab
details
signatures
description
- cve
solution
].index_with do |key|
public_send(key) # rubocop:disable GitlabSecurity/PublicSend
@@ -141,7 +135,7 @@ module Gitlab
def <=>(other)
if severity == other.severity
- compare_key <=> other.compare_key
+ uuid <=> other.uuid
else
::Enums::Vulnerability.severity_levels[other.severity] <=>
::Enums::Vulnerability.severity_levels[severity]
@@ -200,7 +194,7 @@ module Gitlab
private
def generate_project_fingerprint
- Digest::SHA1.hexdigest(compare_key)
+ Digest::SHA1.hexdigest(uuid.to_s)
end
def location_fingerprints
diff --git a/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb b/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb
index e0a4f879f48..b8a964be59a 100644
--- a/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb
@@ -30,21 +30,6 @@ module Gitlab::UsageDataCounters
Gitlab::Template::GitlabCiYmlTemplate.find(template_name.chomp('.gitlab-ci.yml'))&.full_name
end
- def all_included_templates(template_name)
- expanded_template_name = expand_template_name(template_name)
- results = [expanded_template_name].tap do |result|
- template = Gitlab::Template::GitlabCiYmlTemplate.find(template_name.chomp('.gitlab-ci.yml'))
- data = Gitlab::Ci::Config::Yaml::Loader.new(template.content).load.content
- [data[:include]].compact.flatten.each do |ci_include|
- if ci_include_template = ci_include[:template]
- result.concat(all_included_templates(ci_include_template))
- end
- end
- end
-
- results.uniq.sort_by { _1['name'] }
- end
-
private
def template_to_event_name(template)
diff --git a/lib/tasks/gitlab/usage_data.rake b/lib/tasks/gitlab/usage_data.rake
index 1cd72ee6a1b..09db25735d7 100644
--- a/lib/tasks/gitlab/usage_data.rake
+++ b/lib/tasks/gitlab/usage_data.rake
@@ -48,39 +48,5 @@ namespace :gitlab do
FileUtils.mkdir_p(path)
File.write(File.join(path, 'sql_metrics_queries.json'), Gitlab::Json.pretty_generate(queries))
end
-
- # Events for templates included via YAML-less Auto-DevOps
- def implicit_auto_devops_includes
- Gitlab::UsageDataCounters::CiTemplateUniqueCounter
- .all_included_templates('Auto-DevOps.gitlab-ci.yml')
- .map { |template| implicit_auto_devops_event(template) }
- .uniq
- .sort_by { _1['name'] }
- end
-
- # Events for templates included in a .gitlab-ci.yml using include:template
- def explicit_template_includes
- Gitlab::UsageDataCounters::CiTemplateUniqueCounter.ci_templates("lib/gitlab/ci/templates/").each_with_object([]) do |template, result|
- expanded_template_name = Gitlab::UsageDataCounters::CiTemplateUniqueCounter.expand_template_name(template)
- next unless expanded_template_name # guard against templates unavailable on FOSS
-
- event_name = Gitlab::UsageDataCounters::CiTemplateUniqueCounter.ci_template_event_name(expanded_template_name, :repository_source)
-
- result << ci_template_event(event_name)
- end
- end
-
- # rubocop:disable Gitlab/NoCodeCoverageComment
- # :nocov: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/299453
- def ci_template_event(event_name)
- { 'name' => event_name }
- end
- # :nocov:
- # rubocop:enable Gitlab/NoCodeCoverageComment
-
- def implicit_auto_devops_event(expanded_template_name)
- event_name = Gitlab::UsageDataCounters::CiTemplateUniqueCounter.ci_template_event_name(expanded_template_name, :auto_devops_source)
- ci_template_event(event_name)
- end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 80b35158b51..36c98f8e6fd 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10516,9 +10516,6 @@ msgstr ""
msgid "CiCatalog|Remove project from the CI/CD Catalog?"
msgstr ""
-msgid "CiCatalog|Repositories of pipeline components available in this namespace."
-msgstr ""
-
msgid "CiCatalog|Search must be at least 3 characters"
msgstr ""
@@ -16718,6 +16715,9 @@ msgstr ""
msgid "Dependencies|Toggle vulnerability list"
msgstr ""
+msgid "Dependencies|Unknown path"
+msgstr ""
+
msgid "Dependencies|Unsupported file(s) detected"
msgstr ""
@@ -24692,6 +24692,9 @@ msgstr ""
msgid "IdentityVerification|Didn't receive a code? %{codeLinkStart}Send a new code%{codeLinkEnd} or %{phoneLinkStart}enter a new phone number%{phoneLinkEnd}"
msgstr ""
+msgid "IdentityVerification|Didn't receive a code? Send a new code in %{timer} or %{phoneLinkStart}enter a new phone number%{phoneLinkEnd}"
+msgstr ""
+
msgid "IdentityVerification|Email update is only offered once."
msgstr ""
@@ -24761,6 +24764,9 @@ msgstr ""
msgid "IdentityVerification|Send code"
msgstr ""
+msgid "IdentityVerification|Send code in %{timer}"
+msgstr ""
+
msgid "IdentityVerification|Something went wrong. Please try again."
msgstr ""
@@ -28212,6 +28218,12 @@ msgstr ""
msgid "KubernetesDashboard|Running"
msgstr ""
+msgid "KubernetesDashboard|Service"
+msgstr ""
+
+msgid "KubernetesDashboard|Services"
+msgstr ""
+
msgid "KubernetesDashboard|StatefulSet"
msgstr ""
@@ -35817,9 +35829,6 @@ msgstr ""
msgid "Pipelines|CI lint"
msgstr ""
-msgid "Pipelines|CI/CD Catalog"
-msgstr ""
-
msgid "Pipelines|Child pipeline (%{linkStart}parent%{linkEnd})"
msgstr ""
@@ -56332,10 +56341,10 @@ msgstr ""
msgid "You do not belong to any projects yet."
msgstr ""
-msgid "You do not have access to AI features."
+msgid "You do not have access to any projects for creating incidents."
msgstr ""
-msgid "You do not have access to any projects for creating incidents."
+msgid "You do not have access to chat feature."
msgstr ""
msgid "You do not have any subscriptions yet"
diff --git a/qa/Dockerfile b/qa/Dockerfile
index 1a6a670bf96..9911d2a2930 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -1,5 +1,5 @@
ARG DOCKER_VERSION=24.0.5
-ARG CHROME_VERSION=113
+ARG CHROME_VERSION=119
ARG RUBY_VERSION=3.0
ARG QA_BUILD_TARGET=ee
diff --git a/qa/Gemfile b/qa/Gemfile
index 4fc9eee8736..30c86a449fe 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -3,7 +3,7 @@
source 'https://rubygems.org'
gem 'gitlab-qa', '~> 13', '>= 13.1.0', require: 'gitlab/qa'
-gem 'gitlab_quality-test_tooling', '~> 1.10.1', require: false
+gem 'gitlab_quality-test_tooling', '~> 1.11.0', require: false
gem 'gitlab-utils', path: '../gems/gitlab-utils'
gem 'activesupport', '~> 7.0.8' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.23.0'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index b6ccd7e0774..e8035e0d17c 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -128,7 +128,7 @@ GEM
rainbow (>= 3, < 4)
table_print (= 1.5.7)
zeitwerk (>= 2, < 3)
- gitlab_quality-test_tooling (1.10.1)
+ gitlab_quality-test_tooling (1.11.0)
activesupport (>= 6.1, < 7.2)
amatch (~> 0.4.1)
gitlab (~> 4.19)
@@ -360,7 +360,7 @@ DEPENDENCIES
fog-google (~> 1.19)
gitlab-qa (~> 13, >= 13.1.0)
gitlab-utils!
- gitlab_quality-test_tooling (~> 1.10.1)
+ gitlab_quality-test_tooling (~> 1.11.0)
influxdb-client (~> 3.0)
knapsack (~> 4.0)
nokogiri (~> 1.16)
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 7724d4662b6..231909b1a5b 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -8,7 +8,7 @@ module QA
module Env
extend self
- attr_writer :personal_access_token, :admin_personal_access_token
+ attr_writer :personal_access_token, :admin_personal_access_token, :gitlab_url
attr_accessor :dry_run
ENV_VARIABLES = Gitlab::QA::Runtime::Env::ENV_VARIABLES
diff --git a/qa/qa/service/docker_run/gitlab.rb b/qa/qa/service/docker_run/gitlab.rb
index c39f4c22865..11233cbbfee 100644
--- a/qa/qa/service/docker_run/gitlab.rb
+++ b/qa/qa/service/docker_run/gitlab.rb
@@ -4,10 +4,20 @@ module QA
module Service
module DockerRun
class Gitlab < Base
- def initialize(name:, omnibus_config: '', image: '')
+ attr_reader :external_url, :name
+
+ # @param [String] name
+ # @param [String] omnibus_config
+ # @param [String] image
+ # @param [String] ports Docker-formatted port exposition
+ # @see ports https://docs.docker.com/engine/reference/commandline/run/#publish
+ # @param [String] external_url
+ def initialize(name:, omnibus_config: '', image: '', ports: '80:80', external_url: Runtime::Env.gitlab_url)
@image = image
@name = name
@omnibus_configuration = omnibus_config
+ @ports = ports
+ @external_url = external_url
super()
end
@@ -24,7 +34,7 @@ module QA
docker run -d --rm
--network #{network}
--hostname #{host_name}
- --publish 80:80
+ --publish #{@ports}
#{RUBY_PLATFORM.include?('arm64') ? '--platform linux/amd64' : ''}
--env GITLAB_OMNIBUS_CONFIG="#{@omnibus_configuration}"
--name #{@name}
diff --git a/qa/qa/service/gitlab/instances.rb b/qa/qa/service/gitlab/instances.rb
new file mode 100644
index 00000000000..f3ebedec694
--- /dev/null
+++ b/qa/qa/service/gitlab/instances.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module QA
+ module Service
+ module Gitlab
+ class Instances
+ attr_reader :list
+
+ def initialize
+ @list = []
+ end
+
+ # Default omnibus configuration for a GitLab instance
+ # @param cell_url [String] the external url for the GitLab instance
+ def omnibus_configuration(cell_url:)
+ <<~OMNIBUS
+ gitlab_rails['lfs_enabled'] = true;
+ gitlab_rails['initial_root_password']= '#{Runtime::Env.initial_root_password}'
+ external_url '#{cell_url}';
+ OMNIBUS
+ end
+
+ # Sets the gitlab_url values so that gitlab-qa flows work on one of the instances
+ # @param instance [DockerRun::GitLab object] the GitLab instance to be used
+ def set_gitlab_urls(instance)
+ Support::GitlabAddress.define_gitlab_address_attribute!(instance.external_url)
+ Runtime::Env.gitlab_url = instance.external_url
+ Runtime::Scenario.define(:gitlab_address, instance.external_url)
+ end
+
+ # Creates a DockerRun::Gitlab instance and adds to the list of instances
+ # @param name [string] the name for the instance
+ # @param url [string] the URL for the instance
+ # @param external_port [string] the external port
+ # @param internal_port [string] the internal port to use instead of default (optional)
+ # @param omnibus_config [string] omnibus_configuration to use instead of default (optional)
+ # @return [Service::DockerRun::Gitlab] the last created GitLab instance
+ def add_gitlab_instance(name:, url:, external_port:, internal_port: '80', omnibus_config: nil)
+ cell_url = "http://#{url}/"
+ external_url = "http://#{url}:#{external_port}/"
+ ports = "#{external_port}:#{internal_port}"
+ omnibus_config ||= omnibus_configuration(cell_url: cell_url)
+ @list << Service::DockerRun::Gitlab.new(
+ image: Runtime::Env.release,
+ name: name,
+ ports: ports,
+ omnibus_config: omnibus_config,
+ external_url: external_url).tap do |gitlab|
+ gitlab.login
+ gitlab.register!
+ end
+
+ @list.last
+ end
+
+ # Waits for an instance to be healthy
+ # @param instance [DockerRun::GitLab object] the GitLab instance to be checked
+ def wait_for_instance(instance)
+ Support::Waiter.wait_until(max_duration: 900, sleep_interval: 10, raise_on_failure: true) do
+ instance.health == "healthy"
+ end
+ end
+
+ def wait_for_all_instances
+ @list.each { |el| wait_for_instance(el) }
+ end
+
+ def remove_all_instances
+ @list.each(&:remove!)
+
+ @list.clear
+ end
+
+ # Remove an instance with a given name
+ # @param instance_name [String] the name of the instance that was specified during initialization
+ def remove_instance(instance_name)
+ index = @list.index { |x| x.name == instance_name }
+ instance = @list.slice!(index)
+ instance.remove!
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/9_data_stores/cells/login_across_multiple_cells_spec.rb b/qa/qa/specs/features/browser_ui/9_data_stores/cells/login_across_multiple_cells_spec.rb
new file mode 100644
index 00000000000..2724dc2761d
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/9_data_stores/cells/login_across_multiple_cells_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Data Stores', :skip_live_env, :requires_admin, product_group: :tenant_scale do
+ describe 'Multiple Cells' do
+ let(:url) { 'gitlab-cells.bridge' }
+
+ let(:cells) { Service::Gitlab::Instances.new }
+ let!(:first_cell) do
+ cells.add_gitlab_instance(name: 'gitlab-first-cell',
+ external_port: '3000',
+ url: url)
+ end
+
+ let!(:second_cell) do
+ cells.add_gitlab_instance(name: 'gitlab-second-cell',
+ external_port: '3001',
+ url: url)
+ end
+
+ before do
+ cells.set_gitlab_urls(first_cell)
+
+ cells.wait_for_all_instances
+
+ # TODO: configure cells to be connected
+
+ page.visit first_cell.external_url
+ end
+
+ after do
+ cells.remove_all_instances
+ end
+
+ it(
+ 'user logged into one Cell is logged into all',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/433542',
+ only: :local
+ ) do
+ Flow::Login.sign_in(as: create(:user))
+
+ page.visit second_cell.external_url
+
+ Page::Main::Menu.perform do |form|
+ expect(form).to be_signed_in
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/9_data_stores/cells/multiple_cells_gdk_spec.rb b/qa/qa/specs/features/browser_ui/9_data_stores/cells/multiple_cells_gdk_spec.rb
new file mode 100644
index 00000000000..c6240df2a7c
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/9_data_stores/cells/multiple_cells_gdk_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# version of the login test that only runs against GDK
+
+module QA
+ RSpec.describe 'Data Stores', :skip_live_env, :requires_admin, product_group: :tenant_scale do
+ describe 'Multiple Cells' do
+ it(
+ 'user logged into one Cell is logged into all',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/433548',
+ only: :local
+ ) do
+ Flow::Login.sign_in(as: create(:user))
+
+ page.visit ENV.fetch('CELL2_URL')
+
+ Page::Main::Menu.perform do |form|
+ expect(form).to be_signed_in
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/helpers/context_selector.rb b/qa/qa/specs/helpers/context_selector.rb
index aab2a82d90e..fff69ae6ccc 100644
--- a/qa/qa/specs/helpers/context_selector.rb
+++ b/qa/qa/specs/helpers/context_selector.rb
@@ -34,6 +34,7 @@ module QA
options.each do |option|
opts[:domain] = production_domain(uri_tld) if option == :production
+ return run_locally? if option == :local
next unless option.is_a?(Hash)
@@ -57,6 +58,10 @@ module QA
private
+ def run_locally?
+ !Runtime::Env.running_in_ci?
+ end
+
def evaluate_pipeline_context(pipeline)
return true if Runtime::Env.ci_project_name.blank?
diff --git a/qa/spec/specs/helpers/context_selector_spec.rb b/qa/spec/specs/helpers/context_selector_spec.rb
index a4573d96523..d18b88578a4 100644
--- a/qa/spec/specs/helpers/context_selector_spec.rb
+++ b/qa/spec/specs/helpers/context_selector_spec.rb
@@ -324,6 +324,27 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
end
end
+ context 'local' do
+ it 'runs locally' do
+ stub_env('CI_JOB_NAME', nil)
+ group = describe_successfully 'Runs locally', :local do
+ it('runs locally') {}
+ end
+
+ expect(group.examples[0].execution_result.status).to eq(:passed)
+ end
+
+ it 'does not run in CI' do
+ stub_env('CI_JOB_NAME', 'ee:instance-image')
+
+ group = describe_successfully 'Does not run in CI' do
+ it('does not run in CI', only: :local) {}
+ end
+
+ expect(group.examples[0].execution_result.status).to eq(:pending)
+ end
+ end
+
context 'production' do
before do
allow(GitlabEdition).to receive(:jh?).and_return(false)
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index 60b0db1eaf2..b0dc3bf2925 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -340,9 +340,18 @@ function retry_failed_rspec_examples() {
scripts/merge-reports "rspec/rspec-${CI_JOB_ID}.json" "${json_retry_file}"
junit_merge "${junit_retry_file}" "rspec/rspec-${CI_JOB_ID}.xml" --update-only
+ # The tests are flaky because they succeeded after being retried.
if [[ $rspec_run_status -eq 0 ]]; then
- # The test is flaky because it succeeded after being retried.
- # Make the pipeline "pass with warnings" if the flaky test is part of this MR.
+ # "53557338" is the project ID of https://gitlab.com/gitlab-org/quality/engineering-productivity/flaky-tests
+ if [ "$CREATE_RAILS_FLAKY_TEST_ISSUES" == "true" ]; then
+ bundle exec flaky-test-issues \
+ --token "${RAILS_FLAKY_TEST_PROJECT_TOKEN}" \
+ --project "53557338" \
+ --merge_request_iid "$CI_MERGE_REQUEST_IID" \
+ --input-files "rspec/rspec-retry-*.json" || true # We don't want this command to fail the job.
+ fi
+
+ # Make the pipeline "pass with warnings" if the flaky tests are part of this MR.
warn_on_successfully_retried_test
fi
diff --git a/spec/factories/ci/reports/security/findings.rb b/spec/factories/ci/reports/security/findings.rb
index 670d833c1f8..2de17115934 100644
--- a/spec/factories/ci/reports/security/findings.rb
+++ b/spec/factories/ci/reports/security/findings.rb
@@ -2,7 +2,6 @@
FactoryBot.define do
factory :ci_reports_security_finding, class: '::Gitlab::Ci::Reports::Security::Finding' do
- compare_key { "#{identifiers.first&.external_type}:#{identifiers.first&.external_id}:#{location.fingerprint}" }
confidence { :medium }
identifiers { Array.new(1) { association(:ci_reports_security_identifier) } }
location factory: :ci_reports_security_locations_sast
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 807df94e115..2f55c3ab567 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -18,6 +18,10 @@ FactoryBot.define do
create(:namespace_settings, namespace: group) unless group.namespace_settings
end
+ trait :with_organization do
+ association :organization
+ end
+
trait :public do
visibility_level { Gitlab::VisibilityLevel::PUBLIC }
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index c0aaa7f818a..aac2e3c4e46 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -205,7 +205,10 @@ RSpec.describe 'Group', feature_category: :groups_and_projects do
describe 'not showing personalization questions on group creation when it is enabled' do
before do
stub_application_setting(hide_third_party_offers: true)
- visit new_group_path(anchor: 'create-group-pane')
+
+ # If visiting directly via path, personalization setting is not being picked up correctly
+ visit new_group_path
+ click_link 'Create group'
end
it 'does not render personalization questions' do
@@ -350,10 +353,16 @@ RSpec.describe 'Group', feature_category: :groups_and_projects do
visit path
end
- it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="group[name]"]', submit: 'button[type="submit"]' },
- { form: '.js-general-settings-form', input: '#group_visibility_level_0', submit: 'button[type="submit"]' },
- { form: '.js-general-permissions-form', input: '#group_request_access_enabled', submit: 'button[type="submit"]' },
- { form: '.js-general-permissions-form', input: 'input[name="group[two_factor_grace_period]"]', submit: 'button[type="submit"]' }]
+ it_behaves_like 'dirty submit form', [
+ { form: '.js-general-settings-form', input: 'input[name="group[name]"]', submit: 'button[type="submit"]' },
+ { form: '.js-general-settings-form', input: '#group_visibility_level_0', submit: 'button[type="submit"]' },
+ { form: '.js-general-permissions-form', input: '#group_request_access_enabled', submit: 'button[type="submit"]' },
+ {
+ form: '.js-general-permissions-form',
+ input: 'input[name="group[two_factor_grace_period]"]',
+ submit: 'button[type="submit"]'
+ }
+ ]
it 'saves new settings' do
page.within('.gs-general') do
diff --git a/spec/frontend/environments/helpers/k8s_integration_helper_spec.js b/spec/frontend/environments/helpers/k8s_integration_helper_spec.js
index 97100557ef3..852b5318c77 100644
--- a/spec/frontend/environments/helpers/k8s_integration_helper_spec.js
+++ b/spec/frontend/environments/helpers/k8s_integration_helper_spec.js
@@ -1,5 +1,4 @@
import {
- generateServicePortsString,
getDeploymentsStatuses,
getDaemonSetStatuses,
getStatefulSetStatuses,
@@ -12,35 +11,6 @@ import {
import { CLUSTER_AGENT_ERROR_MESSAGES } from '~/environments/constants';
describe('k8s_integration_helper', () => {
- describe('generateServicePortsString', () => {
- const port = '8080';
- const protocol = 'TCP';
- const nodePort = '31732';
-
- it('returns empty string if no ports provided', () => {
- expect(generateServicePortsString([])).toBe('');
- });
-
- it('returns port and protocol when provided', () => {
- expect(generateServicePortsString([{ port, protocol }])).toBe(`${port}/${protocol}`);
- });
-
- it('returns port, protocol and nodePort when provided', () => {
- expect(generateServicePortsString([{ port, protocol, nodePort }])).toBe(
- `${port}:${nodePort}/${protocol}`,
- );
- });
-
- it('returns joined strings of ports if multiple are provided', () => {
- expect(
- generateServicePortsString([
- { port, protocol },
- { port, protocol, nodePort },
- ]),
- ).toBe(`${port}/${protocol}, ${port}:${nodePort}/${protocol}`);
- });
- });
-
describe('getDeploymentsStatuses', () => {
const pending = {
status: {
diff --git a/spec/frontend/kubernetes_dashboard/graphql/mock_data.js b/spec/frontend/kubernetes_dashboard/graphql/mock_data.js
index 63224950a1e..8f733d382b2 100644
--- a/spec/frontend/kubernetes_dashboard/graphql/mock_data.js
+++ b/spec/frontend/kubernetes_dashboard/graphql/mock_data.js
@@ -513,3 +513,87 @@ export const mockCronJobsTableItems = [
];
export const k8sCronJobsMock = [readyCronJob, suspendedCronJob, failedCronJob];
+
+export const k8sServicesMock = [
+ {
+ metadata: {
+ name: 'my-first-service',
+ namespace: 'default',
+ creationTimestamp: '2023-07-31T11:50:17Z',
+ labels: {},
+ annotations: {},
+ },
+ spec: {
+ ports: [
+ {
+ name: 'https',
+ protocol: 'TCP',
+ port: 443,
+ targetPort: 8443,
+ },
+ ],
+ clusterIP: '10.96.0.1',
+ externalIP: '-',
+ type: 'ClusterIP',
+ },
+ },
+ {
+ metadata: {
+ name: 'my-second-service',
+ namespace: 'default',
+ creationTimestamp: '2023-11-21T11:50:59Z',
+ labels: {},
+ annotations: {},
+ },
+ spec: {
+ ports: [
+ {
+ name: 'http',
+ protocol: 'TCP',
+ appProtocol: 'http',
+ port: 80,
+ targetPort: 'http',
+ nodePort: 31989,
+ },
+ {
+ name: 'https',
+ protocol: 'TCP',
+ appProtocol: 'https',
+ port: 443,
+ targetPort: 'https',
+ nodePort: 32679,
+ },
+ ],
+ clusterIP: '10.105.219.238',
+ externalIP: '-',
+ type: 'NodePort',
+ },
+ },
+];
+
+export const mockServicesTableItems = [
+ {
+ name: 'my-first-service',
+ namespace: 'default',
+ type: 'ClusterIP',
+ clusterIP: '10.96.0.1',
+ externalIP: '-',
+ ports: '443/TCP',
+ age: '114d',
+ labels: {},
+ annotations: {},
+ kind: 'Service',
+ },
+ {
+ name: 'my-second-service',
+ namespace: 'default',
+ type: 'NodePort',
+ clusterIP: '10.105.219.238',
+ externalIP: '-',
+ ports: '80:31989/TCP, 443:32679/TCP',
+ age: '1d',
+ labels: {},
+ annotations: {},
+ kind: 'Service',
+ },
+];
diff --git a/spec/frontend/kubernetes_dashboard/graphql/resolvers/kubernetes_spec.js b/spec/frontend/kubernetes_dashboard/graphql/resolvers/kubernetes_spec.js
index 9be7ca2877b..01e2c3d2716 100644
--- a/spec/frontend/kubernetes_dashboard/graphql/resolvers/kubernetes_spec.js
+++ b/spec/frontend/kubernetes_dashboard/graphql/resolvers/kubernetes_spec.js
@@ -7,6 +7,7 @@ import k8sDashboardReplicaSetsQuery from '~/kubernetes_dashboard/graphql/queries
import k8sDashboardDaemonSetsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_daemon_sets.query.graphql';
import k8sDashboardJobsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_jobs.query.graphql';
import k8sDashboardCronJobsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_cron_jobs.query.graphql';
+import k8sDashboardServicesQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql';
import {
k8sPodsMock,
k8sDeploymentsMock,
@@ -15,6 +16,7 @@ import {
k8sDaemonSetsMock,
k8sJobsMock,
k8sCronJobsMock,
+ k8sServicesMock,
} from '../mock_data';
describe('~/frontend/environments/graphql/resolvers', () => {
@@ -624,4 +626,86 @@ describe('~/frontend/environments/graphql/resolvers', () => {
).rejects.toThrow('API error');
});
});
+
+ describe('k8sServices', () => {
+ const client = { writeQuery: jest.fn() };
+
+ const mockWatcher = WatchApi.prototype;
+ const mockServicesListWatcherFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve(mockWatcher);
+ });
+
+ const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
+ if (eventName === 'data') {
+ callback([]);
+ }
+ });
+
+ const mockServicesListFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ items: k8sServicesMock,
+ });
+ });
+
+ const mockAllServicesListFn = jest.fn().mockImplementation(mockServicesListFn);
+
+ describe('when the Services data is present', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
+ .mockImplementation(mockAllServicesListFn);
+ jest.spyOn(mockWatcher, 'subscribeToStream').mockImplementation(mockServicesListWatcherFn);
+ jest.spyOn(mockWatcher, 'on').mockImplementation(mockOnDataFn);
+ });
+
+ it('should request all Services from the cluster_client library and watch the events', async () => {
+ const Services = await mockResolvers.Query.k8sServices(
+ null,
+ {
+ configuration,
+ },
+ { client },
+ );
+
+ expect(mockAllServicesListFn).toHaveBeenCalled();
+ expect(mockServicesListWatcherFn).toHaveBeenCalled();
+
+ expect(Services).toEqual(k8sServicesMock);
+ });
+
+ it('should update cache with the new data when received from the library', async () => {
+ await mockResolvers.Query.k8sServices(null, { configuration, namespace: '' }, { client });
+
+ expect(client.writeQuery).toHaveBeenCalledWith({
+ query: k8sDashboardServicesQuery,
+ variables: { configuration, namespace: '' },
+ data: { k8sServices: [] },
+ });
+ });
+ });
+
+ it('should not watch Services from the cluster_client library when the Services data is not present', async () => {
+ jest.spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces').mockImplementation(
+ jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ items: [],
+ });
+ }),
+ );
+
+ await mockResolvers.Query.k8sServices(null, { configuration }, { client });
+
+ expect(mockServicesListWatcherFn).not.toHaveBeenCalled();
+ });
+
+ it('should throw an error if the API call fails', async () => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
+
+ await expect(
+ mockResolvers.Query.k8sServices(null, { configuration }, { client }),
+ ).rejects.toThrow('API error');
+ });
+ });
});
diff --git a/spec/frontend/kubernetes_dashboard/helpers/k8s_integration_helper_spec.js b/spec/frontend/kubernetes_dashboard/helpers/k8s_integration_helper_spec.js
index 4d6ea2742f2..1fd89e67e79 100644
--- a/spec/frontend/kubernetes_dashboard/helpers/k8s_integration_helper_spec.js
+++ b/spec/frontend/kubernetes_dashboard/helpers/k8s_integration_helper_spec.js
@@ -5,6 +5,7 @@ import {
calculateDaemonSetStatus,
calculateJobStatus,
calculateCronJobStatus,
+ generateServicePortsString,
} from '~/kubernetes_dashboard/helpers/k8s_integration_helper';
import { useFakeDate } from 'helpers/fake_date';
@@ -140,4 +141,33 @@ describe('k8s_integration_helper', () => {
expect(calculateCronJobStatus(item)).toBe(expected);
});
});
+
+ describe('generateServicePortsString', () => {
+ const port = '8080';
+ const protocol = 'TCP';
+ const nodePort = '31732';
+
+ it('returns empty string if no ports provided', () => {
+ expect(generateServicePortsString([])).toBe('');
+ });
+
+ it('returns port and protocol when provided', () => {
+ expect(generateServicePortsString([{ port, protocol }])).toBe(`${port}/${protocol}`);
+ });
+
+ it('returns port, protocol and nodePort when provided', () => {
+ expect(generateServicePortsString([{ port, protocol, nodePort }])).toBe(
+ `${port}:${nodePort}/${protocol}`,
+ );
+ });
+
+ it('returns joined strings of ports if multiple are provided', () => {
+ expect(
+ generateServicePortsString([
+ { port, protocol },
+ { port, protocol, nodePort },
+ ]),
+ ).toBe(`${port}/${protocol}, ${port}:${nodePort}/${protocol}`);
+ });
+ });
});
diff --git a/spec/frontend/kubernetes_dashboard/pages/services_page_spec.js b/spec/frontend/kubernetes_dashboard/pages/services_page_spec.js
new file mode 100644
index 00000000000..c76f4330cd6
--- /dev/null
+++ b/spec/frontend/kubernetes_dashboard/pages/services_page_spec.js
@@ -0,0 +1,104 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import ServicesPage from '~/kubernetes_dashboard/pages/services_page.vue';
+import WorkloadLayout from '~/kubernetes_dashboard/components/workload_layout.vue';
+import { SERVICES_TABLE_FIELDS } from '~/kubernetes_dashboard/constants';
+import { useFakeDate } from 'helpers/fake_date';
+import { k8sServicesMock, mockServicesTableItems } from '../graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('Kubernetes dashboard services page', () => {
+ let wrapper;
+
+ const configuration = {
+ basePath: 'kas/tunnel/url',
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const findWorkloadLayout = () => wrapper.findComponent(WorkloadLayout);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sServices: jest.fn().mockReturnValue(k8sServicesMock),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider = createApolloProvider()) => {
+ wrapper = shallowMount(ServicesPage, {
+ provide: { configuration },
+ apolloProvider,
+ });
+ };
+
+ describe('mounted', () => {
+ it('renders WorkloadLayout component', () => {
+ createWrapper();
+
+ expect(findWorkloadLayout().exists()).toBe(true);
+ });
+
+ it('sets loading prop for the WorkloadLayout', () => {
+ createWrapper();
+
+ expect(findWorkloadLayout().props('loading')).toBe(true);
+ });
+
+ it('removes loading prop from the WorkloadLayout when the list of services loaded', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findWorkloadLayout().props('loading')).toBe(false);
+ });
+ });
+
+ describe('when gets services data', () => {
+ useFakeDate(2023, 10, 23, 10, 10);
+
+ it('sets correct stats object for the WorkloadLayout', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findWorkloadLayout().props('stats')).toEqual([]);
+ });
+
+ it('sets correct table items object for the WorkloadLayout', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findWorkloadLayout().props('items')).toMatchObject(mockServicesTableItems);
+ expect(findWorkloadLayout().props('fields')).toMatchObject(SERVICES_TABLE_FIELDS);
+ });
+ });
+
+ describe('when gets an error from the cluster_client API', () => {
+ const error = new Error('Error from the cluster_client API');
+ const createErroredApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sServices: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ beforeEach(async () => {
+ createWrapper(createErroredApolloProvider());
+ await waitForPromises();
+ });
+
+ it('sets errorMessage prop for the WorkloadLayout', () => {
+ expect(findWorkloadLayout().props('errorMessage')).toBe(error.message);
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 9296e548081..85166549771 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -264,7 +264,7 @@ describe('MrWidgetOptions', () => {
expect(findMergePipelineForkAlert().exists()).toBe(false);
});
- it('hides the alert when merge pipelines are not enabled', async () => {
+ it('hides the alert when merged results pipelines are not enabled', async () => {
createComponent({
updatedMrData: {
source_project_id: 1,
@@ -275,7 +275,7 @@ describe('MrWidgetOptions', () => {
expect(findMergePipelineForkAlert().exists()).toBe(false);
});
- it('shows the alert when merge pipelines are enabled and the source project and target project are different', async () => {
+ it('shows the alert when merged results pipelines are enabled and the source project and target project are different', async () => {
createComponent({
updatedMrData: {
source_project_id: 1,
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
index 38d54eff872..a755f35332f 100644
--- a/spec/frontend/vue_shared/components/gl_countdown_spec.js
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -44,6 +44,10 @@ describe('GlCountdown', () => {
it('displays 00:00:00', () => {
expect(wrapper.text()).toContain('00:00:00');
});
+
+ it('emits `timer-expired` event', () => {
+ expect(wrapper.emitted('timer-expired')).toStrictEqual([[]]);
+ });
});
describe('when an invalid date is passed', () => {
diff --git a/spec/helpers/ci/catalog/resources_helper_spec.rb b/spec/helpers/ci/catalog/resources_helper_spec.rb
index 5c5d02ce6d8..68d56437249 100644
--- a/spec/helpers/ci/catalog/resources_helper_spec.rb
+++ b/spec/helpers/ci/catalog/resources_helper_spec.rb
@@ -36,18 +36,6 @@ RSpec.describe Ci::Catalog::ResourcesHelper, feature_category: :pipeline_composi
end
end
- describe '#can_view_namespace_catalog?' do
- subject { helper.can_view_namespace_catalog?(project) }
-
- before do
- stub_licensed_features(ci_namespace_catalog: false)
- end
-
- it 'user cannot view the Catalog in CE regardless of permissions' do
- expect(subject).to be false
- end
- end
-
describe '#js_ci_catalog_data' do
let(:project) { build(:project, :repository) }
diff --git a/spec/lib/click_house/iterator_spec.rb b/spec/lib/click_house/iterator_spec.rb
index fd054c0afe5..962ccc6d884 100644
--- a/spec/lib/click_house/iterator_spec.rb
+++ b/spec/lib/click_house/iterator_spec.rb
@@ -29,6 +29,16 @@ RSpec.describe ClickHouse::Iterator, :click_house, feature_category: :database d
expect(collect_ids_with_batch_size(15)).to match_array(expected_values)
end
+ context 'when min value is given' do
+ let(:iterator) { described_class.new(query_builder: query_builder, connection: connection, min_value: 5) }
+
+ it 'iterates from the given min value' do
+ expected_values = (5..10).to_a
+
+ expect(collect_ids_with_batch_size(5)).to match_array(expected_values)
+ end
+ end
+
context 'when there are no records for the given query' do
let(:query_builder) do
ClickHouse::QueryBuilder
diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
index bfcc87179af..6aa526c1829 100644
--- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
@@ -185,7 +185,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnera
context 'when name is provided' do
it 'sets name from the report as a name' do
- finding = report.findings.find { |x| x.compare_key == 'CVE-1030' }
+ finding = report.findings.second
expected_name = Gitlab::Json.parse(finding.raw_metadata)['name']
expect(finding.name).to eq(expected_name)
@@ -197,7 +197,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnera
let(:location) { nil }
it 'returns only identifier name' do
- finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' }
+ finding = report.findings.third
+
expect(finding.name).to eq("CVE-2017-11429")
end
end
@@ -205,21 +206,24 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnera
context 'when location exists' do
context 'when CVE identifier exists' do
it 'combines identifier with location to create name' do
- finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' }
+ finding = report.findings.third
+
expect(finding.name).to eq("CVE-2017-11429 in yarn.lock")
end
end
context 'when CWE identifier exists' do
it 'combines identifier with location to create name' do
- finding = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' }
+ finding = report.findings.fourth
+
expect(finding.name).to eq("CWE-2017-11429 in yarn.lock")
end
end
context 'when neither CVE nor CWE identifier exist' do
it 'combines identifier with location to create name' do
- finding = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' }
+ finding = report.findings.fifth
+
expect(finding.name).to eq("other-2017-11429 in yarn.lock")
end
end
diff --git a/spec/lib/gitlab/ci/reports/security/report_spec.rb b/spec/lib/gitlab/ci/reports/security/report_spec.rb
index d7f967f1c55..dabee0f32de 100644
--- a/spec/lib/gitlab/ci/reports/security/report_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/report_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Reports::Security::Report do
+RSpec.describe Gitlab::Ci::Reports::Security::Report, feature_category: :vulnerability_management do
let_it_be(:pipeline) { create(:ci_pipeline) }
let(:created_at) { 2.weeks.ago }
@@ -89,7 +89,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Report do
let(:other_report) do
create(
:ci_reports_security_report,
- findings: [create(:ci_reports_security_finding, compare_key: 'other_finding')],
+ findings: [create(:ci_reports_security_finding)],
scanners: [create(:ci_reports_security_scanner, external_id: 'other_scanner', name: 'Other Scanner')],
identifiers: [create(:ci_reports_security_identifier, external_id: 'other_id', name: 'other_scanner')]
)
diff --git a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
index 2c9506dd498..05938fa08cd 100644
--- a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
@@ -50,18 +50,6 @@ RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter, feature_categ
end
end
- context 'with implicit includes', :snowplow do
- let(:config_source) { :auto_devops_source }
-
- described_class.all_included_templates('Auto-DevOps.gitlab-ci.yml').each do |template_name|
- context "for #{template_name}" do
- let(:template_path) { Gitlab::Template::GitlabCiYmlTemplate.find(template_name.delete_suffix('.gitlab-ci.yml')).full_name }
-
- include_examples 'tracks template'
- end
- end
- end
-
it 'expands short template names' do
expect do
described_class.track_unique_project_event(project: project, template: 'Dependency-Scanning.gitlab-ci.yml', config_source: :repository_source, user: user)
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 9419bfa76a7..bd74af9b7e8 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -1084,6 +1084,110 @@ RSpec.describe Member, feature_category: :groups_and_projects do
end
end
+ context 'for updating organization_users' do
+ let_it_be(:group) { create(:group, :with_organization) }
+ let(:member) { create(:group_member, source: group) }
+ let(:update_organization_users_enabled) { true }
+
+ before do
+ stub_feature_flags(update_organization_users: update_organization_users_enabled)
+ end
+
+ context 'when update_organization_users is enabled' do
+ it 'inserts new record on member creation' do
+ expect { member }.to change { Organizations::OrganizationUser.count }.by(1)
+ record_attrs = { organization: group.organization, user: member.user, access_level: :default }
+ expect(Organizations::OrganizationUser.exists?(record_attrs)).to be(true)
+ end
+
+ context 'when user already exists in the organization_users' do
+ context 'for an already existing default organization_user' do
+ let_it_be(:project) { create(:project, group: group, organization: group.organization) }
+
+ before do
+ member
+ end
+
+ it 'does not insert a new record in organization_users' do
+ expect do
+ create(:project_member, :owner, source: project, user: member.user)
+ end.not_to change { Organizations::OrganizationUser.count }
+
+ expect(
+ Organizations::OrganizationUser.exists?(
+ organization: project.organization, user: member.user, access_level: :default
+ )
+ ).to be(true)
+ end
+
+ it 'does not update timestamps' do
+ travel_to(1.day.from_now) do
+ expect do
+ create(:project_member, :owner, source: project, user: member.user)
+ end.not_to change { Organizations::OrganizationUser.last.updated_at }
+ end
+ end
+ end
+
+ context 'for an already existing owner organization_user' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:common_attrs) { { organization: group.organization, user: user } }
+
+ before_all do
+ create(:organization_user, :owner, common_attrs)
+ end
+
+ it 'does not insert a new record in organization_users nor update the access_level' do
+ expect do
+ create(:group_member, :owner, source: group, user: user)
+ end.not_to change { Organizations::OrganizationUser.count }
+
+ expect(
+ Organizations::OrganizationUser.exists?(common_attrs.merge(access_level: :default))
+ ).to be(false)
+ expect(
+ Organizations::OrganizationUser.exists?(common_attrs.merge(access_level: :owner))
+ ).to be(true)
+ end
+ end
+ end
+
+ context 'when updating the organization_users is not successful' do
+ it 'rolls back the member creation' do
+ allow(Organizations::OrganizationUser).to receive(:upsert).once.and_raise(ActiveRecord::StatementTimeout)
+
+ expect { member }.to raise_error(ActiveRecord::StatementTimeout)
+ expect(Organizations::OrganizationUser.exists?(organization: group.organization)).to be(false)
+ expect(group.group_members).to be_empty
+ end
+ end
+ end
+
+ shared_examples_for 'does not create an organization_user entry' do
+ specify do
+ expect { member }.not_to change { Organizations::OrganizationUser.count }
+ end
+ end
+
+ context 'when update_organization_users is disabled' do
+ let(:update_organization_users_enabled) { false }
+
+ it_behaves_like 'does not create an organization_user entry'
+ end
+
+ context 'when member is an invite' do
+ let(:member) { create(:group_member, :invited, source: group) }
+
+ it_behaves_like 'does not create an organization_user entry'
+ end
+
+ context 'when organization does not exist' do
+ let(:member) { create(:group_member) }
+
+ it_behaves_like 'does not create an organization_user entry'
+ end
+ end
+
context 'when after_commit :update_highest_role' do
let_it_be(:user) { create(:user) }
diff --git a/spec/models/users/phone_number_validation_spec.rb b/spec/models/users/phone_number_validation_spec.rb
index 788df05763c..eb73fc31dac 100644
--- a/spec/models/users/phone_number_validation_spec.rb
+++ b/spec/models/users/phone_number_validation_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resiliency do
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:user) { create(:user) }
let_it_be(:banned_user) { create(:user, :banned) }
@@ -250,4 +252,43 @@ RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resilie
it { is_expected.to be_nil }
end
end
+
+ describe '.sms_send_allowed_after' do
+ let_it_be(:record) { create(:phone_number_validation, sms_send_count: 0) }
+
+ subject(:result) { record.sms_send_allowed_after }
+
+ context 'when there are no attempts yet' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when sms_send_wait_time feature flag is disabled' do
+ let_it_be(:record) { create(:phone_number_validation, sms_send_count: 1) }
+
+ before do
+ stub_feature_flags(sms_send_wait_time: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ where(:attempt_number, :expected_delay) do
+ 2 | 1.minute
+ 3 | 3.minutes
+ 4 | 5.minutes
+ 5 | 10.minutes
+ 6 | 10.minutes
+ end
+
+ with_them do
+ it 'returns the correct delayed timestamp value' do
+ freeze_time do
+ record.update!(sms_send_count: attempt_number - 1, sms_sent_at: Time.current)
+
+ expected_result = Time.current + expected_delay
+ expect(result).to eq expected_result
+ end
+ end
+ end
+ end
end
diff --git a/spec/policies/organizations/organization_policy_spec.rb b/spec/policies/organizations/organization_policy_spec.rb
index a1a2f1db305..9660ed578f7 100644
--- a/spec/policies/organizations/organization_policy_spec.rb
+++ b/spec/policies/organizations/organization_policy_spec.rb
@@ -32,8 +32,19 @@ RSpec.describe Organizations::OrganizationPolicy, feature_category: :cell do
end
context 'when the user is part of the organization' do
- before do
- create :organization_user, organization: organization, user: current_user
+ before_all do
+ create(:organization_user, organization: organization, user: current_user)
+ end
+
+ it { is_expected.to be_disallowed(:admin_organization) }
+ it { is_expected.to be_allowed(:create_group) }
+ it { is_expected.to be_allowed(:read_organization) }
+ it { is_expected.to be_allowed(:read_organization_user) }
+ end
+
+ context 'when the user is an owner of the organization' do
+ before_all do
+ create(:organization_user, :owner, organization: organization, user: current_user)
end
it { is_expected.to be_allowed(:admin_organization) }
diff --git a/spec/requests/api/graphql/mutations/organizations/update_spec.rb b/spec/requests/api/graphql/mutations/organizations/update_spec.rb
index 4e819c280d0..33890ae4592 100644
--- a/spec/requests/api/graphql/mutations/organizations/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/organizations/update_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Mutations::Organizations::Update, feature_category: :cell do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:organization) do
- create(:organization) { |org| create(:organization_user, organization: org, user: user) }
+ create(:organization) { |org| create(:organization_user, :owner, organization: org, user: user) }
end
let(:mutation) { graphql_mutation(:organization_update, params) }
diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb
index 73bcfe81d76..14becd52e93 100644
--- a/spec/requests/api/graphql/organizations/organization_query_spec.rb
+++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb
@@ -67,13 +67,13 @@ RSpec.describe 'getting organization information', feature_category: :cell do
it 'returns correct organization user fields' do
request_organization
- organization_user_node = graphql_data_at(:organization, :organizationUsers, :nodes).first
+ organization_user_nodes = graphql_data_at(:organization, :organizationUsers, :nodes)
expected_attributes = {
"badges" => [{ "text" => "It's you!", "variant" => 'muted' }],
"id" => organization_user.to_global_id.to_s,
"user" => { "id" => user.to_global_id.to_s }
}
- expect(organization_user_node).to match(expected_attributes)
+ expect(organization_user_nodes).to include(expected_attributes)
end
it 'avoids N+1 queries for all the fields' do
diff --git a/spec/requests/organizations/settings_controller_spec.rb b/spec/requests/organizations/settings_controller_spec.rb
index 1d98e598159..0177187e3a3 100644
--- a/spec/requests/organizations/settings_controller_spec.rb
+++ b/spec/requests/organizations/settings_controller_spec.rb
@@ -21,13 +21,13 @@ RSpec.describe Organizations::SettingsController, feature_category: :cell do
end
context 'when the user is signed in' do
+ let_it_be(:user) { create(:user) }
+
before do
sign_in(user)
end
context 'with no association to an organization' do
- let_it_be(:user) { create(:user) }
-
it_behaves_like 'organization - not found response'
it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag'
end
@@ -39,11 +39,18 @@ RSpec.describe Organizations::SettingsController, feature_category: :cell do
it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag'
end
- context 'as an organization user' do
- let_it_be(:user) { create :user }
+ context 'as a default organization user' do
+ before_all do
+ create(:organization_user, organization: organization, user: user)
+ end
- before do
- create :organization_user, organization: organization, user: user
+ it_behaves_like 'organization - not found response'
+ it_behaves_like 'organization - action disabled by `ui_for_organizations` feature flag'
+ end
+
+ context 'as an owner of an organization' do
+ before_all do
+ create(:organization_user, :owner, organization: organization, user: user)
end
it_behaves_like 'organization - successful response'
diff --git a/spec/services/ci/catalog/resources/create_service_spec.rb b/spec/services/ci/catalog/resources/create_service_spec.rb
index 202c76acaec..5839b9ac2fe 100644
--- a/spec/services/ci/catalog/resources/create_service_spec.rb
+++ b/spec/services/ci/catalog/resources/create_service_spec.rb
@@ -8,10 +8,6 @@ RSpec.describe Ci::Catalog::Resources::CreateService, feature_category: :pipelin
let(:service) { described_class.new(project, user) }
- before do
- stub_licensed_features(ci_namespace_catalog: true)
- end
-
describe '#execute' do
context 'with an unauthorized user' do
it 'raises an AccessDeniedError' do
diff --git a/spec/services/ci/catalog/resources/destroy_service_spec.rb b/spec/services/ci/catalog/resources/destroy_service_spec.rb
index da5ba7ad0bc..4783506416d 100644
--- a/spec/services/ci/catalog/resources/destroy_service_spec.rb
+++ b/spec/services/ci/catalog/resources/destroy_service_spec.rb
@@ -9,10 +9,6 @@ RSpec.describe Ci::Catalog::Resources::DestroyService, feature_category: :pipeli
let(:service) { described_class.new(project, user) }
- before do
- stub_licensed_features(ci_namespace_catalog: true)
- end
-
describe '#execute' do
context 'with an unauthorized user' do
it 'raises an AccessDeniedError' do
diff --git a/spec/services/organizations/update_service_spec.rb b/spec/services/organizations/update_service_spec.rb
index 630bfdfe1d7..30c07ae1d13 100644
--- a/spec/services/organizations/update_service_spec.rb
+++ b/spec/services/organizations/update_service_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Organizations::UpdateService, feature_category: :cell do
context 'when user has permission' do
before_all do
- create(:organization_user, organization: organization, user: current_user)
+ create(:organization_user, :owner, organization: organization, user: current_user)
end
shared_examples 'updating an organization' do
diff --git a/spec/services/security/merge_reports_service_spec.rb b/spec/services/security/merge_reports_service_spec.rb
index c141bbe5b5a..a65e73bd8ce 100644
--- a/spec/services/security/merge_reports_service_spec.rb
+++ b/spec/services/security/merge_reports_service_spec.rb
@@ -20,7 +20,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_1_primary, identifier_1_cve],
scanner: scanner_1,
- severity: :low
+ severity: :low,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94610'
)
end
@@ -29,7 +30,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_1_primary, identifier_1_cve],
scanner: scanner_1,
- severity: :low
+ severity: :low,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94611'
)
end
@@ -39,7 +41,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
identifiers: [identifier_2_primary, identifier_2_cve],
location: build(:ci_reports_security_locations_sast, start_line: 32, end_line: 34),
scanner: scanner_2,
- severity: :medium
+ severity: :medium,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94614'
)
end
@@ -49,7 +52,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
identifiers: [identifier_2_primary, identifier_2_cve],
location: build(:ci_reports_security_locations_sast, start_line: 32, end_line: 34),
scanner: scanner_2,
- severity: :medium
+ severity: :medium,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94613'
)
end
@@ -59,7 +63,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
identifiers: [identifier_2_primary, identifier_2_cve],
location: build(:ci_reports_security_locations_sast, start_line: 42, end_line: 44),
scanner: scanner_2,
- severity: :medium
+ severity: :medium,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94612'
)
end
@@ -68,7 +73,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_cwe],
scanner: scanner_3,
- severity: :high
+ severity: :high,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94615'
)
end
@@ -77,7 +83,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_cwe],
scanner: scanner_1,
- severity: :critical
+ severity: :critical,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94616'
)
end
@@ -86,7 +93,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_wasc],
scanner: scanner_1,
- severity: :medium
+ severity: :medium,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94617'
)
end
@@ -95,7 +103,8 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
:ci_reports_security_finding,
identifiers: [identifier_wasc],
scanner: scanner_2,
- severity: :critical
+ severity: :critical,
+ uuid: '61eb8e3e-3be1-4d6c-ba26-4e0dd4f94618'
)
end
@@ -226,9 +235,32 @@ RSpec.describe Security::MergeReportsService, '#execute', feature_category: :cod
let(:identifier_cve) { build(:ci_reports_security_identifier, external_id: 'CVE-2019-123', external_type: 'cve') }
let(:identifier_semgrep) { build(:ci_reports_security_identifier, external_id: 'rules.bandit.B105', external_type: 'semgrep_id') }
- let(:finding_id_1) { build(:ci_reports_security_finding, identifiers: [identifier_bandit, identifier_cve], scanner: bandit_scanner, report_type: :sast) }
- let(:finding_id_2) { build(:ci_reports_security_finding, identifiers: [identifier_cve], scanner: semgrep_scanner, report_type: :sast) }
- let(:finding_id_3) { build(:ci_reports_security_finding, identifiers: [identifier_semgrep], scanner: semgrep_scanner, report_type: :sast) }
+ let(:finding_id_1) do
+ build(
+ :ci_reports_security_finding,
+ identifiers: [identifier_bandit, identifier_cve],
+ scanner: bandit_scanner,
+ report_type: :sast,
+ uuid: '21ab978a-7052-5428-af0b-c7a4b3fe5020')
+ end
+
+ let(:finding_id_2) do
+ build(
+ :ci_reports_security_finding,
+ identifiers: [identifier_cve],
+ scanner: semgrep_scanner,
+ report_type: :sast,
+ uuid: '21ab978a-7052-5428-af0b-c7a4b3fe5021')
+ end
+
+ let(:finding_id_3) do
+ build(
+ :ci_reports_security_finding,
+ identifiers: [identifier_semgrep],
+ scanner: semgrep_scanner,
+ report_type: :sast,
+ uuid: '21ab978a-7052-5428-af0b-c7a4b3fe5022')
+ end
let(:bandit_report) do
build(:ci_reports_security_report,
diff --git a/spec/validators/ip_cidr_array_validator_spec.rb b/spec/validators/ip_cidr_array_validator_spec.rb
index 6adb0bc70db..f18005054b5 100644
--- a/spec/validators/ip_cidr_array_validator_spec.rb
+++ b/spec/validators/ip_cidr_array_validator_spec.rb
@@ -17,7 +17,6 @@ RSpec.describe IpCidrArrayValidator, feature_category: :shared do
using RSpec::Parameterized::TableSyntax
- # noinspection RubyMismatchedArgumentType - https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-32041
where(:cidr_array, :validity, :errors) do
# rubocop:disable Layout/LineLength -- The RSpec table syntax often requires long lines for errors
nil | false | { cidr_array: ["must be an array of CIDR values"] }
diff --git a/spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb b/spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb
new file mode 100644
index 00000000000..d4fa35b9b82
--- /dev/null
+++ b/spec/workers/click_house/event_authors_consistency_cron_worker_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ClickHouse::EventAuthorsConsistencyCronWorker, feature_category: :value_stream_management do
+ let(:worker) { described_class.new }
+
+ context 'when ClickHouse is disabled' do
+ it 'does nothing' do
+ allow(ClickHouse::Client).to receive(:database_configured?).and_return(false)
+
+ expect(worker).not_to receive(:log_extra_metadata_on_done)
+
+ worker.perform
+ end
+ end
+
+ context 'when the event_sync_worker_for_click_house feature flag is off' do
+ it 'does nothing' do
+ allow(ClickHouse::Client).to receive(:database_configured?).and_return(true)
+ stub_feature_flags(event_sync_worker_for_click_house: false)
+
+ expect(worker).not_to receive(:log_extra_metadata_on_done)
+
+ worker.perform
+ end
+ end
+
+ context 'when ClickHouse is available', :click_house do
+ let_it_be(:connection) { ClickHouse::Connection.new(:main) }
+ let_it_be_with_reload(:user1) { create(:user) }
+ let_it_be_with_reload(:user2) { create(:user) }
+
+ let(:leftover_author_ids) { connection.select('SELECT DISTINCT author_id FROM events FINAL').pluck('author_id') }
+ let(:deleted_user_id1) { user2.id + 1 }
+ let(:deleted_user_id2) { user2.id + 2 }
+
+ before do
+ insert_query = <<~SQL
+ INSERT INTO events (id, author_id) VALUES
+ (1, #{user1.id}),
+ (2, #{user2.id}),
+ (3, #{deleted_user_id1}),
+ (4, #{deleted_user_id1}),
+ (5, #{deleted_user_id2})
+ SQL
+
+ connection.execute(insert_query)
+ end
+
+ it 'cleans up all inconsistent records in ClickHouse' do
+ worker.perform
+
+ expect(leftover_author_ids).to contain_exactly(user1.id, user2.id)
+
+ # the next job starts from the beginning of the table
+ expect(ClickHouse::SyncCursor.cursor_for(:event_authors_consistency_check)).to eq(0)
+ end
+
+ context 'when the previous job was not finished' do
+ it 'continues the processing from the cursor' do
+ ClickHouse::SyncCursor.update_cursor_for(:event_authors_consistency_check, deleted_user_id1)
+
+ worker.perform
+
+ # the previous records should remain
+ expect(leftover_author_ids).to contain_exactly(user1.id, user2.id)
+ end
+ end
+
+ context 'when processing stops due to the record clean up limit' do
+ it 'stores the last processed id value' do
+ User.where(id: [user1.id, user2.id]).delete_all
+
+ stub_const("#{described_class}::MAX_AUTHOR_DELETIONS", 2)
+ stub_const("#{described_class}::POSTGRESQL_BATCH_SIZE", 1)
+
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:result,
+ { status: :deletion_limit_reached, deletions: 2 })
+
+ worker.perform
+
+ expect(leftover_author_ids).to contain_exactly(deleted_user_id1, deleted_user_id2)
+ expect(ClickHouse::SyncCursor.cursor_for(:event_authors_consistency_check)).to eq(user2.id)
+ end
+ end
+
+ context 'when time limit is reached' do
+ it 'stops the processing earlier' do
+ stub_const("#{described_class}::POSTGRESQL_BATCH_SIZE", 1)
+
+ # stop at the third author_id
+ allow_next_instance_of(Analytics::CycleAnalytics::RuntimeLimiter) do |runtime_limiter|
+ allow(runtime_limiter).to receive(:over_time?).and_return(false, false, true)
+ end
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:result, { status: :over_time, deletions: 1 })
+
+ worker.perform
+
+ expect(leftover_author_ids).to contain_exactly(user1.id, user2.id, deleted_user_id2)
+ end
+ end
+ end
+end