diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 3441be8f99b..36d6d8a3973 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -291,6 +291,7 @@ .bundler-patterns: &bundler-patterns - '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}' + - '{Gemfile.next.lock,*/Gemfile.next.lock,*/*/Gemfile.next.lock}' .nodejs-patterns: &nodejs-patterns - '{package.json,*/package.json,*/*/package.json}' @@ -301,6 +302,7 @@ .dependency-patterns: &dependency-patterns - '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}' + - '{Gemfile.next.lock,*/Gemfile.next.lock,*/*/Gemfile.next.lock}' - '{go.sum,*/go.sum,*/*/go.sum}' - '{yarn.lock,*/yarn.lock,*/*/yarn.lock}' @@ -322,6 +324,7 @@ .assets-compilation-patterns: &assets-compilation-patterns - "{package.json,yarn.lock}" - "{Gemfile,Gemfile.lock}" + - "{Gemfile.next,Gemfile.next.lock}" - ".browserslistrc" - "babel.config.js" - "config/webpack.config.js" @@ -352,6 +355,7 @@ - "**/Rakefile" - "**/Dangerfile" - "**/Gemfile" + - "**/Gemfile.next" - "**/Guardfile" - "**/*.rake" - "**/*.rb" @@ -360,6 +364,7 @@ # Backend patterns + .ci-patterns .backend-patterns: &backend-patterns - "{,jh/}Gemfile{,.lock}" + - "{,jh/}Gemfile.next{,.lock}" - "Rakefile" - "config.ru" - "keeps/**/*" @@ -378,6 +383,7 @@ .search-backend-patterns: &search-backend-patterns - "{,jh/}Gemfile.lock" + - "{,jh/}Gemfile.next.lock" - "GITLAB_ELASTICSEARCH_INDEXER_VERSION" # List explicitly all the app/ dirs that are backend (i.e. all except app/assets). - "{,ee/,jh/}{app/channels,app/components,app/controllers,app/finders,app/graphql,app/helpers,app/mailers,app/models,app/policies,app/presenters,app/serializers,app/services,app/uploaders,app/validators,app/views,app/workers}/**/*" @@ -470,6 +476,7 @@ - ".stylelintrc" - "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*" - "{,jh/}Gemfile{,.lock}" + - "{,jh/}Gemfile.next{,.lock}" - "{package.json,yarn.lock}" - "*_VERSION" - "lib/gitlab/redis/*" @@ -497,6 +504,7 @@ - ".stylelintrc" - "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*" - "{,jh/}Gemfile{,.lock}" + - "{,jh/}Gemfile.next{,.lock}" - "{package.json,yarn.lock}" - "*_VERSION" - "babel.config.js" @@ -531,6 +539,7 @@ - ".stylelintrc" - "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*" - "{,jh/}Gemfile{,.lock}" + - "{,jh/}Gemfile.next{,.lock}" - "{package.json,yarn.lock}" - "*_VERSION" - "babel.config.js" @@ -560,6 +569,7 @@ - ".stylelintrc" - "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*" - "{,jh/}Gemfile{,.lock}" + - "{,jh/}Gemfile.next{,.lock}" - "{package.json,yarn.lock}" - "*_VERSION" - "babel.config.js" @@ -605,6 +615,7 @@ - ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" - "*_VERSION" - "{,jh/}Gemfile{,.lock}" + - "{,jh/}Gemfile.next{,.lock}" - "keeps/**/*" - "Rakefile" - "tests.yml" @@ -653,6 +664,7 @@ - ".rubocop_todo/**/*.yml" - "{,ee/,jh/}rubocop/**/*" # We might be changing custom cops - "{,ee/,jh/}Gemfile.lock" # This should include gitlab-styles, rubocop itself, and any plugins we might be using + - "{,ee/,jh/}Gemfile.next.lock" # This should include gitlab-styles, rubocop itself, and any plugins we might be using - "lib/gitlab_edition.rb" # This is required in RuboCop::CodeReuseHelpers - ".gitlab/ci/static-analysis.gitlab-ci.yml" - "config/feature_categories.yml" # Used by RSpec/FeatureCategory @@ -664,6 +676,7 @@ .core-backend-patterns: &core-backend-patterns - "{,jh/}Gemfile{,.lock}" + - "{,jh/}Gemfile.next{,.lock}" - "{,ee/,jh/}config/**/*.rb" .core-frontend-patterns: &core-frontend-patterns @@ -685,6 +698,7 @@ .gdk-component-patterns: &gdk-component-patterns - qa/gdk/**/* - Gemfile.lock + - Gemfile.next.lock - yarn.lock - scripts/build_gdk_image - scripts/frontend/postinstall.js @@ -2737,7 +2751,7 @@ when: never - <<: *if-schedule-maintenance - <<: *if-merge-request - changes: ["Gemfile.lock"] + changes: ["Gemfile.lock", "Gemfile.next.lock"] .reports:rules:x-ray: rules: diff --git a/.rubocop_todo/gitlab/bounded_contexts.yml b/.rubocop_todo/gitlab/bounded_contexts.yml index 7b74ce08fb6..725e78fcfff 100644 --- a/.rubocop_todo/gitlab/bounded_contexts.yml +++ b/.rubocop_todo/gitlab/bounded_contexts.yml @@ -102,7 +102,6 @@ Gitlab/BoundedContexts: - 'app/finders/personal_access_tokens_finder.rb' - 'app/finders/personal_projects_finder.rb' - 'app/finders/projects_finder.rb' - - 'app/finders/prometheus_metrics_finder.rb' - 'app/finders/protected_branches_finder.rb' - 'app/finders/releases_finder.rb' - 'app/finders/resource_milestone_event_finder.rb' diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml index 7d5c2beb5d7..0f86ae1939d 100644 --- a/.rubocop_todo/gitlab/namespaced_class.yml +++ b/.rubocop_todo/gitlab/namespaced_class.yml @@ -73,7 +73,6 @@ Gitlab/NamespacedClass: - 'app/finders/personal_access_tokens_finder.rb' - 'app/finders/personal_projects_finder.rb' - 'app/finders/projects_finder.rb' - - 'app/finders/prometheus_metrics_finder.rb' - 'app/finders/protected_branches_finder.rb' - 'app/finders/releases_finder.rb' - 'app/finders/resource_milestone_event_finder.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index cbd54049d05..9b9f27cbfd8 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -3108,7 +3108,6 @@ Layout/LineLength: - 'spec/finders/personal_access_tokens_finder_spec.rb' - 'spec/finders/personal_projects_finder_spec.rb' - 'spec/finders/projects/groups_finder_spec.rb' - - 'spec/finders/projects/prometheus/alerts_finder_spec.rb' - 'spec/finders/projects_finder_spec.rb' - 'spec/finders/repositories/tree_finder_spec.rb' - 'spec/finders/security/security_jobs_finder_spec.rb' diff --git a/.rubocop_todo/rspec/any_instance_of.yml b/.rubocop_todo/rspec/any_instance_of.yml index 03d0efe23ea..4fba39936d4 100644 --- a/.rubocop_todo/rspec/any_instance_of.yml +++ b/.rubocop_todo/rspec/any_instance_of.yml @@ -100,7 +100,6 @@ RSpec/AnyInstanceOf: - 'spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb' - 'spec/features/snippets/embedded_snippet_spec.rb' - 'spec/features/usage_stats_consent_spec.rb' - - 'spec/finders/prometheus_metrics_finder_spec.rb' - 'spec/graphql/mutations/alert_management/create_alert_issue_spec.rb' - 'spec/graphql/mutations/alert_management/http_integration/create_spec.rb' - 'spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb' diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml index 53726203e17..1a614f9f1ce 100644 --- a/.rubocop_todo/rspec/feature_category.yml +++ b/.rubocop_todo/rspec/feature_category.yml @@ -1431,9 +1431,7 @@ RSpec/FeatureCategory: - 'spec/finders/projects/groups_finder_spec.rb' - 'spec/finders/projects/members/effective_access_level_finder_spec.rb' - 'spec/finders/projects/members/effective_access_level_per_user_finder_spec.rb' - - 'spec/finders/projects/prometheus/alerts_finder_spec.rb' - 'spec/finders/projects/topics_finder_spec.rb' - - 'spec/finders/prometheus_metrics_finder_spec.rb' - 'spec/finders/protected_branches_finder_spec.rb' - 'spec/finders/releases/evidence_pipeline_finder_spec.rb' - 'spec/finders/resource_milestone_event_finder_spec.rb' diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml index e279ba7091b..4dcfe10e1dc 100644 --- a/.rubocop_todo/rspec/named_subject.yml +++ b/.rubocop_todo/rspec/named_subject.yml @@ -1382,8 +1382,6 @@ RSpec/NamedSubject: - 'spec/finders/projects/members/effective_access_level_per_user_finder_spec.rb' - 'spec/finders/projects/ml/candidate_finder_spec.rb' - 'spec/finders/projects/ml/model_finder_spec.rb' - - 'spec/finders/projects/prometheus/alerts_finder_spec.rb' - - 'spec/finders/prometheus_metrics_finder_spec.rb' - 'spec/finders/releases/evidence_pipeline_finder_spec.rb' - 'spec/finders/releases_finder_spec.rb' - 'spec/finders/repositories/tree_finder_spec.rb' diff --git a/app/assets/javascripts/import_entities/import_groups/constants.js b/app/assets/javascripts/import_entities/import_groups/constants.js index e5236f8d537..aa386135005 100644 --- a/app/assets/javascripts/import_entities/import_groups/constants.js +++ b/app/assets/javascripts/import_entities/import_groups/constants.js @@ -29,11 +29,13 @@ export const TARGET_NAMESPACE_FIELD = 'targetNamespace'; export const ROOT_NAMESPACE = { fullPath: '', id: null }; +export const QUERY_PARAM_FAILED = 'failed'; + const PLACEHOLDER_STATUS_PENDING_REASSIGNMENT = 'PENDING_REASSIGNMENT'; export const PLACEHOLDER_STATUS_AWAITING_APPROVAL = 'AWAITING_APPROVAL'; const PLACEHOLDER_STATUS_REJECTED = 'REJECTED'; export const PLACEHOLDER_STATUS_REASSIGNING = 'REASSIGNMENT_IN_PROGRESS'; -const PLACEHOLDER_STATUS_FAILED = 'FAILED'; +export const PLACEHOLDER_STATUS_FAILED = 'FAILED'; export const PLACEHOLDER_STATUS_KEPT_AS_PLACEHOLDER = 'KEEP_AS_PLACEHOLDER'; export const PLACEHOLDER_STATUS_COMPLETED = 'COMPLETED'; diff --git a/app/assets/javascripts/members/placeholders/components/app.vue b/app/assets/javascripts/members/placeholders/components/app.vue index f93873833cc..21942038b2f 100644 --- a/app/assets/javascripts/members/placeholders/components/app.vue +++ b/app/assets/javascripts/members/placeholders/components/app.vue @@ -4,6 +4,11 @@ import { mapState } from 'vuex'; import { GlBadge, GlTab, GlTabs } from '@gitlab/ui'; import { createAlert } from '~/alert'; import { s__, sprintf } from '~/locale'; +import { getParameterByName } from '~/lib/utils/url_utility'; +import { + PLACEHOLDER_STATUS_FAILED, + QUERY_PARAM_FAILED, +} from '~/import_entities/import_groups/constants'; import importSourceUsersQuery from '../graphql/queries/import_source_users.query.graphql'; import PlaceholdersTable from './placeholders_table.vue'; @@ -40,6 +45,7 @@ export default { fullPath: this.group.path, ...this.cursor, [this.cursor.before ? 'last' : 'first']: DEFAULT_PAGE_SIZE, + statuses: this.queryStatuses, }; }, update(data) { @@ -64,6 +70,16 @@ export default { pageInfo() { return this.sourceUsers?.pageInfo || {}; }, + statusParamValue() { + return getParameterByName('status'); + }, + queryStatuses() { + if (getParameterByName('status') === QUERY_PARAM_FAILED) { + return [PLACEHOLDER_STATUS_FAILED]; + } + + return []; + }, }, mounted() { diff --git a/app/assets/javascripts/members/placeholders/graphql/queries/import_source_users.query.graphql b/app/assets/javascripts/members/placeholders/graphql/queries/import_source_users.query.graphql index c9343237e85..fd2fcc1757c 100644 --- a/app/assets/javascripts/members/placeholders/graphql/queries/import_source_users.query.graphql +++ b/app/assets/javascripts/members/placeholders/graphql/queries/import_source_users.query.graphql @@ -1,10 +1,23 @@ #import "~/graphql_shared/fragments/page_info.fragment.graphql" #import "../fragments/import_source_user.fragment.graphql" -query importSourceUsers($fullPath: ID!, $before: String, $after: String, $first: Int, $last: Int) { +query importSourceUsers( + $fullPath: ID! + $before: String + $after: String + $first: Int + $last: Int + $statuses: [ImportSourceUserStatus!] +) { namespace(fullPath: $fullPath) { id - importSourceUsers(before: $before, after: $after, first: $first, last: $last) { + importSourceUsers( + before: $before + after: $after + first: $first + last: $last + statuses: $statuses + ) { nodes { ...ImportSourceUser } diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_template.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_template.vue index 5f8588eafbb..dc47117de82 100644 --- a/app/assets/javascripts/pages/shared/wikis/components/wiki_template.vue +++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_template.vue @@ -4,6 +4,9 @@ import { escape } from 'lodash'; import { __, sprintf } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import SafeHtml from '~/vue_shared/directives/safe_html'; +import { InternalEvents } from '~/tracking'; + +const trackingMixin = InternalEvents.mixin(); export default { components: { @@ -12,6 +15,7 @@ export default { directives: { SafeHtml, }, + mixins: [trackingMixin], props: { templates: { type: Array, @@ -54,6 +58,7 @@ export default { }, async selectTemplate(templatePath) { const template = await axios.get(templatePath); + this.trackEvent('apply_wiki_template'); this.$emit('input', template.data); }, highlight(text) { diff --git a/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue index b5a11324ecf..6dac93b29d6 100644 --- a/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue +++ b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue @@ -74,7 +74,11 @@ export default { this.search = searchTerm; }, onTokensUpdate(tokens) { - this.$emit('update', tokens); + const uniqueTokens = Array.from(new Map(tokens.map((item) => [item.name, item])).values()); + + this.selectedTokens = uniqueTokens; + + this.$emit('update', this.selectedTokens); }, }, AVATAR_SHAPE_OPTION_RECT, @@ -88,6 +92,7 @@ export default { :dropdown-items="topics" :loading="loading" allow-user-defined-tokens + show-add-new-always :placeholder="placeholderText" @keydown.enter="handleEnter" @text-input="filterTopics" diff --git a/app/controllers/sandbox_controller.rb b/app/controllers/sandbox_controller.rb index dffe6797831..89e01fcaa6a 100644 --- a/app/controllers/sandbox_controller.rb +++ b/app/controllers/sandbox_controller.rb @@ -2,6 +2,7 @@ class SandboxController < ApplicationController # rubocop:disable Gitlab/NamespacedClass skip_before_action :authenticate_user! + skip_before_action :enforce_terms! feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned diff --git a/app/finders/concerns/namespaces/groups_filter.rb b/app/finders/concerns/namespaces/groups_filter.rb new file mode 100644 index 00000000000..d009f0b2e66 --- /dev/null +++ b/app/finders/concerns/namespaces/groups_filter.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Namespaces + module GroupsFilter + private + + def by_search(groups) + return groups unless params[:search].present? + + groups.search(params[:search], include_parents: params[:parent].blank?) + end + + def skip_groups(groups) + return groups unless params[:skip_groups].present? + + groups.id_not_in(params[:skip_groups]) + end + + def min_access_level? + current_user && params[:min_access_level].present? + end + + def sort(groups) + return groups.order_id_desc unless params[:sort] + + groups.sort_by_attribute(params[:sort]) + end + + def by_visibility(groups) + return groups unless params[:visibility] + + groups.by_visibility_level(params[:visibility]) + end + end +end diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index bcc916c04e5..e98b5d5400e 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -29,6 +29,7 @@ # public groups instead, even if `all_available` is set to false. class GroupsFinder < UnionFinder include CustomAttributesFilter + include Namespaces::GroupsFilter attr_reader :current_user, :params @@ -115,12 +116,6 @@ class GroupsFinder < UnionFinder groups.in_organization(organization) end - def by_visibility(groups) - return groups unless params[:visibility] - - groups.by_visibility_level(params[:visibility]) - end - def by_parent(groups) return groups unless params[:parent] @@ -155,18 +150,6 @@ class GroupsFinder < UnionFinder groups.id_not_in(params[:exclude_group_ids]) end - def by_search(groups) - return groups unless params[:search].present? - - groups.search(params[:search], include_parents: params[:parent].blank?) - end - - def sort(groups) - return groups.order_id_desc unless params[:sort] - - groups.sort_by_attribute(params[:sort]) - end - def include_parent_shared_groups? params.fetch(:include_parent_shared_groups, false) end @@ -175,10 +158,6 @@ class GroupsFinder < UnionFinder params.fetch(:include_parent_descendants, false) end - def min_access_level? - current_user && params[:min_access_level].present? - end - def include_public_groups? current_user.nil? || all_available? end diff --git a/app/finders/namespaces/groups/shared_groups_finder.rb b/app/finders/namespaces/groups/shared_groups_finder.rb index 54dce93e8ff..c8537306df6 100644 --- a/app/finders/namespaces/groups/shared_groups_finder.rb +++ b/app/finders/namespaces/groups/shared_groups_finder.rb @@ -12,7 +12,8 @@ # module Namespaces module Groups - class SharedGroupsFinder < GroupsFinder + class SharedGroupsFinder + include Namespaces::GroupsFilter include Gitlab::Allowable attr_reader :group, :current_user, :params @@ -35,6 +36,15 @@ module Namespaces def filter_shared_groups(groups) by_visibility(groups) + .then { |filtered_groups| skip_groups(filtered_groups) } + .then { |filtered_groups| by_search(filtered_groups) } + .then { |filtered_groups| by_min_access_level(filtered_groups) } + end + + def by_min_access_level(groups) + return groups unless min_access_level? + + groups.by_min_access_level(current_user, params[:min_access_level]) end end end diff --git a/app/finders/projects/prometheus/alerts_finder.rb b/app/finders/projects/prometheus/alerts_finder.rb deleted file mode 100644 index 2105516db5f..00000000000 --- a/app/finders/projects/prometheus/alerts_finder.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -module Projects - module Prometheus - # Find Prometheus alerts by +project+, +environment+, +id+, - # or any combo thereof. - # - # Optionally filter by +metric+. - # - # Arguments: - # params: - # project: Project | integer - # environment: Environment | integer - # metric: PrometheusMetric | integer - class AlertsFinder - def initialize(params = {}) - unless params[:project] || params[:environment] || params[:id] - raise ArgumentError, - 'Please provide one or more of the following params: :project, :environment, :id' - end - - @params = params - end - - # Find all matching alerts - # - # @return [ActiveRecord::Relation] - def execute - relation = by_project(PrometheusAlert) - relation = by_environment(relation) - relation = by_metric(relation) - relation = by_id(relation) - ordered(relation) - end - - private - - attr_reader :params - - def by_project(relation) - return relation unless params[:project] - - relation.for_project(params[:project]) - end - - def by_environment(relation) - return relation unless params[:environment] - - relation.for_environment(params[:environment]) - end - - def by_metric(relation) - return relation unless params[:metric] - - relation.for_metric(params[:metric]) - end - - def by_id(relation) - return relation unless params[:id] - - relation.id_in(params[:id]) - end - - def ordered(relation) - relation.order_by('id_asc') - end - end - end -end diff --git a/app/finders/prometheus_metrics_finder.rb b/app/finders/prometheus_metrics_finder.rb deleted file mode 100644 index 346514e3265..00000000000 --- a/app/finders/prometheus_metrics_finder.rb +++ /dev/null @@ -1,132 +0,0 @@ -# frozen_string_literal: true - -class PrometheusMetricsFinder - ACCEPTED_PARAMS = [ - :project, - :group, - :title, - :y_label, - :identifier, - :id, - :common, - :ordered - ].freeze - - # Cautiously preferring a memoized class method over a constant - # so that the DB connection is accessed after the class is loaded. - def self.indexes - @indexes ||= PrometheusMetric - .connection - .indexes(:prometheus_metrics) - .map { |index| index.columns.map(&:to_sym) } - end - - def initialize(params = {}) - @params = params.slice(*ACCEPTED_PARAMS) - end - - # @return [PrometheusMetric, PrometheusMetric::ActiveRecord_Relation] - def execute - validate_params! - - metrics = by_project(::PrometheusMetric.all) - metrics = by_group(metrics) - metrics = by_title(metrics) - metrics = by_y_label(metrics) - metrics = by_common(metrics) - metrics = by_ordered(metrics) - metrics = by_identifier(metrics) - by_id(metrics) - end - - private - - attr_reader :params - - def by_project(metrics) - return metrics unless params[:project] - - metrics.for_project(params[:project]) - end - - def by_group(metrics) - return metrics unless params[:group] - - metrics.for_group(params[:group]) - end - - def by_title(metrics) - return metrics unless params[:title] - - metrics.for_title(params[:title]) - end - - def by_y_label(metrics) - return metrics unless params[:y_label] - - metrics.for_y_label(params[:y_label]) - end - - def by_common(metrics) - return metrics unless params[:common] - - metrics.common - end - - def by_ordered(metrics) - return metrics unless params[:ordered] - - metrics.ordered - end - - def by_identifier(metrics) - return metrics unless params[:identifier] - - metrics.for_identifier(params[:identifier]) - end - - def by_id(metrics) - return metrics unless params[:id] - - metrics.id_in(params[:id]) - end - - def validate_params! - validate_params_present! - validate_id_params! - validate_indexes! - end - - # Ensure all provided params are supported - def validate_params_present! - raise ArgumentError, "Please provide one or more of: #{ACCEPTED_PARAMS}" if params.blank? - end - - # Protect against the caller "finding" the wrong metric - def validate_id_params! - raise ArgumentError, 'Only one of :identifier, :id is permitted' if params[:identifier] && params[:id] - - return unless params[:identifier] && !(params[:project] || params[:common]) - - raise ArgumentError, ':identifier must be scoped to a :project or :common' - end - - # Protect against unaccounted-for, complex/slow queries. - # This is not a hard and fast rule, but is meant to encourage - # mindful inclusion of new queries. - def validate_indexes! - indexable_params = params.except(:ordered, :id, :project).keys - indexable_params << :project_id if params[:project] - indexable_params.sort! - - return if appropriate_index?(indexable_params) - - raise ArgumentError, "An index should exist for params: #{indexable_params}" - end - - def appropriate_index?(indexable_params) - return true if indexable_params.blank? - - self.class.indexes.any? { |index| (index - indexable_params).empty? } - end -end diff --git a/app/models/group.rb b/app/models/group.rb index 1bb8a516869..b8a087d9b58 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -301,6 +301,7 @@ class Group < Namespace scope :order_path_asc, -> { reorder(self.arel_table['path'].asc) } scope :order_path_desc, -> { reorder(self.arel_table['path'].desc) } scope :in_organization, ->(organization) { where(organization: organization) } + scope :by_min_access_level, ->(user, access_level) { joins(:group_members).where(members: { user: user }).where('members.access_level >= ?', access_level) } class << self def sort_by_attribute(method) diff --git a/app/services/personal_access_tokens/last_used_service.rb b/app/services/personal_access_tokens/last_used_service.rb index 3e8da55bc95..4e402a44dec 100644 --- a/app/services/personal_access_tokens/last_used_service.rb +++ b/app/services/personal_access_tokens/last_used_service.rb @@ -18,7 +18,7 @@ module PersonalAccessTokens # would be updated when using #touch). return unless update? - with_lease do + try_obtain_lease do ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do @personal_access_token.update_column(:last_used_at, Time.zone.now) end @@ -35,18 +35,6 @@ module PersonalAccessTokens @lease_key ||= "pat:last_used_update_lock:#{@personal_access_token.id}" end - def with_lease - return yield unless Feature.enabled?( - :use_lease_for_pat_last_used_update, - Feature.current_request, - type: :gitlab_com_derisk - ) - - try_obtain_lease do - yield - end - end - def update? return false if ::Gitlab::Database.read_only? diff --git a/config/events/apply_wiki_template.yml b/config/events/apply_wiki_template.yml new file mode 100644 index 00000000000..733b4c544cc --- /dev/null +++ b/config/events/apply_wiki_template.yml @@ -0,0 +1,18 @@ +--- +description: Wiki template is applied to a page +internal_events: true +action: apply_wiki_template +identifiers: +- project +- namespace +- user +product_group: knowledge +milestone: '17.3' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161130 +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/config/feature_flags/gitlab_com_derisk/use_lease_for_pat_last_used_update.yml b/config/feature_flags/gitlab_com_derisk/use_lease_for_pat_last_used_update.yml deleted file mode 100644 index 349e999bc69..00000000000 --- a/config/feature_flags/gitlab_com_derisk/use_lease_for_pat_last_used_update.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: use_lease_for_pat_last_used_update -feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/468851 -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158577 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/471276 -milestone: '17.2' -group: group::scalability -type: gitlab_com_derisk -default_enabled: false diff --git a/config/gitlab_loose_foreign_keys.yml b/config/gitlab_loose_foreign_keys.yml index 5a458b5425f..950d5055131 100644 --- a/config/gitlab_loose_foreign_keys.yml +++ b/config/gitlab_loose_foreign_keys.yml @@ -397,6 +397,10 @@ security_scans: - table: p_ci_builds column: build_id on_delete: async_delete +security_trainings: + - table: projects + column: project_id + on_delete: async_delete snippets: - table: organizations column: organization_id diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_apply_wiki_template_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_apply_wiki_template_monthly.yml new file mode 100644 index 00000000000..d2dfeb4f5e8 --- /dev/null +++ b/config/metrics/counts_28d/count_distinct_user_id_from_apply_wiki_template_monthly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_apply_wiki_template_monthly +description: Monthly count of unique users who applied a template to wiki page +product_group: knowledge +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.3' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161130 +time_frame: 28d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: apply_wiki_template + unique: user.id diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_apply_wiki_template_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_apply_wiki_template_weekly.yml new file mode 100644 index 00000000000..52791124a45 --- /dev/null +++ b/config/metrics/counts_7d/count_distinct_user_id_from_apply_wiki_template_weekly.yml @@ -0,0 +1,22 @@ +--- +key_path: redis_hll_counters.count_distinct_user_id_from_apply_wiki_template_weekly +description: Weekly count of unique users who applied a template to wiki page +product_group: knowledge +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.3' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161130 +time_frame: 7d +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: apply_wiki_template + unique: user.id diff --git a/config/metrics/counts_all/count_total_apply_wiki_template.yml b/config/metrics/counts_all/count_total_apply_wiki_template.yml new file mode 100644 index 00000000000..a47c63b3ca9 --- /dev/null +++ b/config/metrics/counts_all/count_total_apply_wiki_template.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.count_total_apply_wiki_template +description: Total count of applied wiki templates +product_group: knowledge +performance_indicator_type: [] +value_type: number +status: active +milestone: '17.3' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161130 +time_frame: all +data_source: internal_events +data_category: optional +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +events: +- name: apply_wiki_template diff --git a/db/docs/batched_background_migrations/nullify_organization_id_for_snippets.yml b/db/docs/batched_background_migrations/nullify_organization_id_for_snippets.yml new file mode 100644 index 00000000000..007244cf30a --- /dev/null +++ b/db/docs/batched_background_migrations/nullify_organization_id_for_snippets.yml @@ -0,0 +1,9 @@ +--- +migration_job_name: NullifyOrganizationIdForSnippets +description: Nullfies organization_id for ProjectSnippets +feature_category: source_code_management +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/160838 +milestone: '17.3' +queued_migration_version: 20240726081618 +finalize_after: '2024-08-26' +finalized_by: # version of the migration that finalized this BBM diff --git a/db/docs/security_training_providers.yml b/db/docs/security_training_providers.yml index e41bde7d27e..02de42919e2 100644 --- a/db/docs/security_training_providers.yml +++ b/db/docs/security_training_providers.yml @@ -7,4 +7,4 @@ feature_categories: description: Stores information about the available security training providers introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78195 milestone: '14.7' -gitlab_schema: gitlab_main_clusterwide +gitlab_schema: gitlab_sec diff --git a/db/docs/security_trainings.yml b/db/docs/security_trainings.yml index 610efb16fac..717d439c932 100644 --- a/db/docs/security_trainings.yml +++ b/db/docs/security_trainings.yml @@ -7,7 +7,7 @@ feature_categories: description: Stores information about the primary security training provider for a given project introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78195 milestone: '14.7' -gitlab_schema: gitlab_main_cell +gitlab_schema: gitlab_sec allow_cross_foreign_keys: - gitlab_main_clusterwide sharding_key: diff --git a/db/post_migrate/20240723091431_remove_projects_security_trainings_project_id_fk.rb b/db/post_migrate/20240723091431_remove_projects_security_trainings_project_id_fk.rb new file mode 100644 index 00000000000..29644a5c95a --- /dev/null +++ b/db/post_migrate/20240723091431_remove_projects_security_trainings_project_id_fk.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class RemoveProjectsSecurityTrainingsProjectIdFk < Gitlab::Database::Migration[2.2] + milestone '17.3' + disable_ddl_transaction! + + FOREIGN_KEY_NAME = "fk_rails_f80240fae0" + + def up + with_lock_retries do + remove_foreign_key_if_exists(:security_trainings, :projects, + name: FOREIGN_KEY_NAME, reverse_lock_order: true) + end + end + + def down + add_concurrent_foreign_key(:security_trainings, :projects, + name: FOREIGN_KEY_NAME, column: :project_id, + target_column: :id, on_delete: :cascade) + end +end diff --git a/db/post_migrate/20240726081618_queue_nullify_organization_id_for_snippets.rb b/db/post_migrate/20240726081618_queue_nullify_organization_id_for_snippets.rb new file mode 100644 index 00000000000..5406b2b0bdf --- /dev/null +++ b/db/post_migrate/20240726081618_queue_nullify_organization_id_for_snippets.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class QueueNullifyOrganizationIdForSnippets < Gitlab::Database::Migration[2.2] + milestone '17.3' + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + MIGRATION = "NullifyOrganizationIdForSnippets" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 1000 + SUB_BATCH_SIZE = 50 + + def up + queue_batched_background_migration( + MIGRATION, + :snippets, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :snippets, :id, []) + end +end diff --git a/db/schema_migrations/20240723091431 b/db/schema_migrations/20240723091431 new file mode 100644 index 00000000000..e95f04cf85c --- /dev/null +++ b/db/schema_migrations/20240723091431 @@ -0,0 +1 @@ +c09bb2913ad4fa49a39694a040e65dbe28506611267e499ad3ad4aa464bd9ab0 \ No newline at end of file diff --git a/db/schema_migrations/20240726081618 b/db/schema_migrations/20240726081618 new file mode 100644 index 00000000000..d2dd1b59caf --- /dev/null +++ b/db/schema_migrations/20240726081618 @@ -0,0 +1 @@ +2f550894b53a31cafe392b25b8927d66279b04bcd44a2315d2f716e7cd975313 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 14f0ff39b85..dde1d3a8667 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -35573,9 +35573,6 @@ ALTER TABLE ONLY internal_ids ALTER TABLE ONLY issues_self_managed_prometheus_alert_events ADD CONSTRAINT fk_rails_f7db2d72eb FOREIGN KEY (self_managed_prometheus_alert_event_id) REFERENCES self_managed_prometheus_alert_events(id) ON DELETE CASCADE; -ALTER TABLE ONLY security_trainings - ADD CONSTRAINT fk_rails_f80240fae0 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; - ALTER TABLE ONLY merge_requests_closing_issues ADD CONSTRAINT fk_rails_f8540692be FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; diff --git a/doc/api/groups.md b/doc/api/groups.md index 5da6e6b7392..ab5a65cb5bc 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -454,7 +454,7 @@ To distinguish between a project in the group and a project shared to the group, ## List a group's shared groups -Get a list of groups shared to this group. When accessed without authentication, only public shared groups are returned. +Get a list of groups where the given group has been invited. When accessed without authentication, only public shared groups are returned. By default, this request returns 20 results at a time because the API results [are paginated](rest/index.md#pagination). @@ -463,10 +463,12 @@ Parameters: | Attribute | Type | Required | Description | | ------------------------------------- | ----------------- | -------- | ---------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) owned by the authenticated user | +| `skip_groups` | array of integers | no | Skip the specified group IDs | | `search` | string | no | Return the list of authorized groups matching the search criteria | | `order_by` | string | no | Order groups by `name`, `path`, `id`, or `similarity`. Default is `name` | | `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | | `visibility` | string | no | Limit to groups with `public`, `internal`, or `private` visibility. | +| `min_access_level` | integer | no | Limit to groups where current user has at least the specified [role (`access_level`)](members.md#roles) | | `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) | ```plaintext diff --git a/doc/operations/index.md b/doc/operations/index.md index a0526d55725..12f91f5531c 100644 --- a/doc/operations/index.md +++ b/doc/operations/index.md @@ -9,6 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w Visualize and analyze errors, traces, metrics and logs collected from your application and its infrastructure. Monitor, identify and resolve performance issues and incidents collaboratively. +- [Getting started](../user/get_started/get_started_monitoring.md) - [Error Tracking](error_tracking.md) - [Distributed tracing](tracing.md) - [Metrics](metrics.md) diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 71f3cddeaf4..bf15cd730aa 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -96,7 +96,7 @@ Audit event types belong to the following product categories. | Name | Description | Saved to database | Streamed | Introduced in | Scope | |:------------|:------------|:------------------|:---------|:--------------|:--------------| -| [`job_artifact_downloaded`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129608) | Triggered when a user download a job artifact from a project | **{dotted-circle}** No | **{check-circle}** Yes | GitLab [16.8](https://gitlab.com/gitlab-org/gitlab/-/issues/250663) | Project | +| [`job_artifact_downloaded`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129608) | Triggered when a user downloads a job artifact from a project | **{dotted-circle}** No | **{check-circle}** Yes | GitLab [16.8](https://gitlab.com/gitlab-org/gitlab/-/issues/250663) | Project | ### Code review @@ -156,7 +156,7 @@ Audit event types belong to the following product categories. | [`member_destroyed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109711) | Event triggered when a membership is destroyed | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/374112) | Group, Project | | [`member_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109711) | Event triggered when a membership is updated | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/374112) | Group, Project | | [`merge_request_create`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90911) | Event triggered when a Merge Request is created | **{dotted-circle}** No | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/367239) | Project | -| [`omniauth_login_failed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123080) | Event triggered when an OmniAuth login fails | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/issues/374107) | User | +| [`omniauth_login_failed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123080) | Triggered when an OmniAuth login fails | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/issues/374107) | User | | [`password_reset_requested`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114548) | Event triggered when a user requests a password reset using a registered email address | **{check-circle}** Yes | **{dotted-circle}** No | GitLab [15.11](https://gitlab.com/gitlab-org/gitlab/-/issues/374107) | User | | [`personal_access_token_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108952) | Event triggered when a user creates a personal access token | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/374113) | User | | [`personal_access_token_revoked`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108952) | Event triggered when a personal access token is revoked | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/374113) | User | @@ -520,7 +520,7 @@ Audit event types belong to the following product categories. | Name | Description | Saved to database | Streamed | Introduced in | Scope | |:------------|:------------|:------------------|:---------|:--------------|:--------------| | [`authenticated_with_group_saml`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28575) | Triggered when successfully signing in with SAML authentication. | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/35710) | Group | -| [`ban_user`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116103) | Event triggered on user ban action | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.11](https://gitlab.com/gitlab-org/gitlab/-/issues/377620) | User | +| [`ban_user`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116103) | Triggered when a user is banned, unbanned, blocked, or unblocked | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.11](https://gitlab.com/gitlab-org/gitlab/-/issues/377620) | User | | [`change_membership_state`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87924) | Event triggered on a users membership is updated | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/362200) | Group | | [`password_reset_failed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129079) | Event triggered when a password reset fails for a user | **{dotted-circle}** No | **{check-circle}** Yes | GitLab [16.4](https://gitlab.com/gitlab-org/gitlab/-/issues/377762) | User | | [`unban_user`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116221) | Event triggered on user unban action | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.11](https://gitlab.com/gitlab-org/gitlab/-/issues/377620) | User | diff --git a/doc/user/get_started/get_started_monitoring.md b/doc/user/get_started/get_started_monitoring.md new file mode 100644 index 00000000000..79622118648 --- /dev/null +++ b/doc/user/get_started/get_started_monitoring.md @@ -0,0 +1,204 @@ +--- +stage: Monitor +group: Observability +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments +description: "Get Started monitoring your application" +--- + +# Get started with monitoring your application in GitLab + +Monitoring is a crucial part of maintaining and optimizing your applications. +GitLab observability features help you track errors, analyze application performance, and respond to incidents. + +These capabilities are part of the larger DevOps workflow: + +![Workflow](img/get_started_monitor_app_v17_3.png) + +All of these features can be used independently. For example, you can use +tracing or incidents without using error tracking. However, for the best experience, +use all of these features together. + +## Step 1: Determine which project to use + +You can use the same project for monitoring that you already use to store your application's source code. + +For large applications with multiple services and repositories, you should create a dedicated project +to centralize all telemetry data collected from the different components of the system. +This approach offers several benefits: + +- Data is accessible to all development and operations teams, which facilitates collaboration. +- Data from different sources can be queried and correlated in one place, which accelerates investigations. +- It provides a single source of truth for all observability data, making it easier to maintain and update. +- It simplifies access management for administrators by centralizing user permissions in a single project. + +To enable observability features, you need administrator or the Owner role for the project. + +For more information, see: + +- [Create a project](../project/index.md) + +## Step 2: Track application errors with error tracking + +Error tracking helps you identify, prioritize, and debug errors in your application. +Errors generated by your application are collected by the Sentry SDK, +then stored on either GitLab or Sentry back ends. + +For more information, see: + +- [How error tracking works](../../operations/error_tracking.md#how-error-tracking-works) + +## Step 3: Monitor application performance with tracing, metrics, and logs + +### Enable beta features + +The following features are available in closed beta: + +- [Distributed tracing](../../operations/tracing.md): Follow application requests across multiple services. +- [Metrics](../../operations/metrics.md): Monitor application and infrastructure performance metrics, + like request latency, traffic, error rate, or saturation. +- [Logs](../../operations/logs.md): Centralize and analyze application and infrastructure logs. + +To make these features available, an administrator must [enable the feature flag](../../administration/feature_flags.md) +named `observability_features` for your project or group. After these features are enabled, you can set up data collection. + +### Instrument your application with OpenTelemetry + +Traces, metrics, and logs are generated from your application and collected +by OpenTelemetry, then stored on the GitLab back end. + +[OpenTelemetry](https://opentelemetry.io/docs/what-is-opentelemetry/) is an open-source +observability framework that provides a collection of tools, APIs, and SDKs for generating, +collecting, and exporting telemetry data. The OpenTelemetry Collector is a key component of this framework. + +You can collect and send telemetry data to GitLab using either direct instrumentation +or the OpenTelemetry Collector. This table compares the two methods: + +| Method | Pros | Cons | +|--------|------|------| +| Direct instrumentation | - Simpler setup
- No infrastructure changes| - Less flexible
- No data sampling or processing
- Can generate high volume of data | +| OpenTelemetry Collector | - Centralized configuration
- Enables data sampling and processing
- Controlled volume of data | - More complex setup
- Requires infrastructure changes | + +You should use the OpenTelemetry Collector for most setups, especially if your application +is likely to grow in complexity. However, direct instrumentation can be simpler for testing purposes and small applications. + +#### Direct instrumentation + +You can instrument your application code to send telemetry data directly to GitLab without using a collector. + +Choose a guide based on your programming language or framework: + +- [Ruby on Rails](../../tutorials/observability/observability_rails_tutorial.md) +- [Node JS](../../tutorials/observability/observability_nodejs_tutorial.md) + +For other languages, use the appropriate [OpenTelemetry API or SDK](https://opentelemetry.io/docs/languages/). + +#### Using the OpenTelemetry Collector (recommended) + +For complex application setups, you should use the OpenTelemetry Collector. + +**What is the OpenTelemetry Collector?** + +The [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) acts like proxy that receives, processes, and exports telemetry data from your application to your monitoring tools such as GitLab Observability. It is opensource and vendor-neutral, which means you can use with any compatible tools and avoid vendor lock-in. + +Benefits of using the Collector: + +- Simplicity: Application services send data to only one destination (the Collector) +- Flexibility: Add or change data destinations from a single place (if you use multiple vendors) +- Advanced features: Sampling, batching and compression of data +- Consistency: Uniform data processing +- Governance: Centralized configuration + +**Configure the OpenTelemetry Collector** + +1. [Quick start installation](https://opentelemetry.io/docs/collector/quick-start/) +1. [Choose a deployment method](https://opentelemetry.io/docs/collector/deployment/) (agent or gateway) +1. [Configure data collection](https://opentelemetry.io/docs/collector/configuration/) + Add the GitLab endpoint as an exporter in the Collector `config.yaml` file: + + ```yaml + exporters: + otlphttp/gitlab: + endpoint: https://observe.gitlab.com/v3///ingest/ + headers: + "private-token": "" + + service: + pipelines: + traces: + exporters: [spanmetrics, otlphttp/gitlab] + metrics: + exporters: [otlphttp/gitlab] + logs: + exporters: [otlphttp/gitlab] + ``` + + Replace the placeholders with the following values: + + - ``: The top-level group ID for your project. + On the group homepage, in the upper-right corner, + select the vertical ellipsis (**{ellipsis_v}**), then **Copy group ID**. + + - ``: The project ID. On the project homepage, + in the upper-right corner, select the vertical ellipsis (**{ellipsis_v}**), then **Copy project ID**. + + - ``: An access token created in the project with the `Owner` role and + `read_api` and `write_observability` scopes. Create a token at the project's **Settings** > **Access tokens**. + +1. Instrument your application to send data to the Collector. + Use the language-specific guides above, but point to your Collector instead of GitLab. + For example, if your application and your Collector are on the same host, send your application to this URL: + + ```plaintext + OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \ + ``` + +### Test your setup + +After setting up data collection, you can visualize the collected data in your project by viewing the **Monitor** navigation menu. +Use the **Tracing**, **Metrics**, and **Logs** pagesto access this information. These features work together to provide a comprehensive view of your application's health and performance, helping you troubleshoot detected issues. + +For more information, see: + +- [Distributed tracing](../../operations/tracing.md) +- [Metrics](../../operations/metrics.md) +- [Logs](../../operations/logs.md) + +## Step 4: Monitor infrastructure with metrics and logs + +To monitor your applications' infrastructure performance and availability +first install the OpenTelemetry Collector as described previously. Then, +based on your setup, you can use various methods to gather metrics and logs data: + +- For host-level, OS metrics: Use the OpenTelemetry Collector with a receiver like + [Host Metrics](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/hostmetricsreceiver). + This receiver collects CPU, memory, disk, and network metrics from the host system. +- For cloud-based infrastructure: Use your provider's monitoring solution integrated with OpenTelemetry. + For example, receivers like [AWS CloudWatch](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/awscloudwatchreceiver) or [Azure Monitor](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/azuremonitorreceiver). +- For containerized applications: Use receivers like + [Docker stats](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/dockerstatsreceiver/) or + [Kubelet stats](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/kubeletstatsreceiver). +- For Kubernetes clusters: Follow [this external guide](https://opentelemetry.io/docs/kubernetes/getting-started/). + +## Step 5: Manage alerts and incidents + +Set up incident management features to troubleshoot issues and resolve incidents collaboratively. + +For more information, see: + +- [Incident Management](../../operations/incident_management/index.md) + +## Step 6: Analyze and improve + +Use the data and insights gathered to continuously improve your application and the monitoring process: + +1. Create insight dashboards to analyze issues + or incidents created and closed, and assess the performance of your incident response. +1. Create executable runbooks to help engineers on-call remediate incidents autonomously. +1. Regularly review your monitoring setup and adjust sampling thresholds or add new metrics as your application evolves. +1. Conduct post-incident reviews to identify areas for improvement in both your application and your incident response process. +1. Use the insights gained from monitoring to inform your development priorities and technical debt reduction efforts. + +For more information, see: + +- [Insight dashboards](../project/insights/index.md) +- [Executable runbooks](../project/clusters/runbooks/index.md) diff --git a/doc/user/get_started/img/get_started_monitor_app_v17_3.png b/doc/user/get_started/img/get_started_monitor_app_v17_3.png new file mode 100644 index 00000000000..d744da38cbe Binary files /dev/null and b/doc/user/get_started/img/get_started_monitor_app_v17_3.png differ diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 9efe3b03016..5b7b3678e60 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -349,8 +349,10 @@ module API tags %w[groups] end params do - optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, - desc: 'Limit by visibility' + optional :skip_groups, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of group ids to exclude from list' + optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'Limit by visibility' + optional :search, type: String, desc: 'Search for a specific group' + optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Minimum access level of authenticated user' optional :order_by, type: String, values: %w[name path id similarity], default: 'name', desc: 'Order by name, path, id or similarity if searching' optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' diff --git a/lib/gitlab/background_migration/nullify_organization_id_for_snippets.rb b/lib/gitlab/background_migration/nullify_organization_id_for_snippets.rb new file mode 100644 index 00000000000..b5d0e83ade8 --- /dev/null +++ b/lib/gitlab/background_migration/nullify_organization_id_for_snippets.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # Remove organization_id from project snippets + # their organization_id will be calculated from the project relation + class NullifyOrganizationIdForSnippets < BatchedMigrationJob + feature_category :source_code_management + operation_name :nullify_organization_id_for_snippets + + def perform + each_sub_batch do |sub_batch| + sub_batch.where(type: 'ProjectSnippet').update_all(organization_id: nil) + end + end + end + end +end diff --git a/spec/dot_gitlab_ci/rules_spec.rb b/spec/dot_gitlab_ci/rules_spec.rb index 854fe237bb3..25a0d47323d 100644 --- a/spec/dot_gitlab_ci/rules_spec.rb +++ b/spec/dot_gitlab_ci/rules_spec.rb @@ -226,6 +226,7 @@ RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', feature_category: :tooling do 'ee/lib/ee/gitlab/background_migration/.rubocop.yml', 'ee/LICENSE', 'Gemfile.checksum', + 'Gemfile.next.checksum', 'gems/error_tracking_open_api/.openapi-generator/FILES', 'gems/error_tracking_open_api/.openapi-generator/VERSION', 'gems/openbao_client/.openapi-generator/FILES', diff --git a/spec/finders/namespaces/groups/shared_groups_finder_spec.rb b/spec/finders/namespaces/groups/shared_groups_finder_spec.rb index 15024680c52..be36f5893bf 100644 --- a/spec/finders/namespaces/groups/shared_groups_finder_spec.rb +++ b/spec/finders/namespaces/groups/shared_groups_finder_spec.rb @@ -7,8 +7,8 @@ RSpec.describe Namespaces::Groups::SharedGroupsFinder, feature_category: :groups let_it_be(:another_user) { create(:user) } let_it_be(:current_user) { user } let_it_be(:group) { create(:group, :private, owners: user, name: "group") } - let_it_be(:shared_group) { create(:group, :private, name: "b#{group.name}") } - let_it_be(:other_group) { create(:group, :private, name: "a#{group.name}") } + let_it_be(:shared_group) { create(:group, :private, name: "shared group") } + let_it_be(:other_group) { create(:group, :public, name: "other group") } let(:params) { {} } @@ -35,5 +35,34 @@ RSpec.describe Namespaces::Groups::SharedGroupsFinder, feature_category: :groups expect(results).to be_empty end end + + context 'with search filter' do + let(:params) { { search: "other group" } } + + it 'filters by search term' do + expect(results).to contain_exactly(other_group) + end + end + + context 'with visibility filter' do + let(:params) { { visibility: 'private' } } + + it 'filters by visibility' do + expect(results).to contain_exactly(shared_group) + end + end + + context 'with min_access_level filter' do + before_all do + shared_group.add_owner(current_user) + other_group.add_owner(current_user) + end + + let(:params) { { min_access_level: Gitlab::Access::OWNER } } + + it 'filters by minimum access level' do + expect(results).to contain_exactly(shared_group, other_group) + end + end end end diff --git a/spec/finders/projects/prometheus/alerts_finder_spec.rb b/spec/finders/projects/prometheus/alerts_finder_spec.rb deleted file mode 100644 index f47f8addbe9..00000000000 --- a/spec/finders/projects/prometheus/alerts_finder_spec.rb +++ /dev/null @@ -1,169 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Projects::Prometheus::AlertsFinder do - let(:finder) { described_class.new(params) } - let(:params) { {} } - - describe 'with params' do - let_it_be(:project) { create(:project) } - let_it_be(:other_project) { create(:project) } - let_it_be(:other_env) { create(:environment, project: other_project) } - let_it_be(:production) { create(:environment, project: project) } - let_it_be(:staging) { create(:environment, project: project) } - let_it_be(:alert) { create_alert(project, production) } - let_it_be(:alert2) { create_alert(project, production) } - let_it_be(:stg_alert) { create_alert(project, staging) } - let_it_be(:other_alert) { create_alert(other_project, other_env) } - - describe '#execute' do - subject { finder.execute } - - context 'with project' do - before do - params[:project] = project - end - - it { is_expected.to eq([alert, alert2, stg_alert]) } - - context 'with matching metric' do - before do - params[:metric] = alert.prometheus_metric - end - - it { is_expected.to eq([alert]) } - end - - context 'with matching metric id' do - before do - params[:metric] = alert.prometheus_metric_id - end - - it { is_expected.to eq([alert]) } - end - - context 'with project non-specific metric' do - before do - params[:metric] = other_alert.prometheus_metric - end - - it { is_expected.to be_empty } - end - end - - context 'with environment' do - before do - params[:environment] = production - end - - it { is_expected.to eq([alert, alert2]) } - - context 'with matching metric' do - before do - params[:metric] = alert.prometheus_metric - end - - it { is_expected.to eq([alert]) } - end - - context 'with environment non-specific metric' do - before do - params[:metric] = stg_alert.prometheus_metric - end - - it { is_expected.to be_empty } - end - end - - context 'with matching project and environment' do - before do - params[:project] = project - params[:environment] = production - end - - it { is_expected.to eq([alert, alert2]) } - - context 'with matching metric' do - before do - params[:metric] = alert.prometheus_metric - end - - it { is_expected.to eq([alert]) } - end - - context 'with environment non-specific metric' do - before do - params[:metric] = stg_alert.prometheus_metric - end - - it { is_expected.to be_empty } - end - - context 'with matching id' do - before do - params[:id] = alert.id - end - - it { is_expected.to eq([alert]) } - end - - context 'with a nil id' do - before do - params[:id] = nil - end - - it { is_expected.to eq([alert, alert2]) } - end - end - - context 'with non-matching project-environment pair' do - before do - params[:project] = project - params[:environment] = other_env - end - - it { is_expected.to be_empty } - end - - context 'with id' do - before do - params[:id] = alert.id - end - - it { is_expected.to eq([alert]) } - end - - context 'with multiple ids' do - before do - params[:id] = [alert.id, other_alert.id] - end - - it { is_expected.to eq([alert, other_alert]) } - end - - context 'with non-matching id' do - before do - params[:id] = -5 - end - - it { is_expected.to be_empty } - end - end - - private - - def create_alert(project, environment) - create(:prometheus_alert, project: project, environment: environment) - end - end - - describe 'without params' do - subject { finder } - - it 'raises an error' do - expect { subject } - .to raise_error(ArgumentError, 'Please provide one or more of the following params: :project, :environment, :id') - end - end -end diff --git a/spec/finders/prometheus_metrics_finder_spec.rb b/spec/finders/prometheus_metrics_finder_spec.rb deleted file mode 100644 index 10ef9f76637..00000000000 --- a/spec/finders/prometheus_metrics_finder_spec.rb +++ /dev/null @@ -1,144 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe PrometheusMetricsFinder do - describe '#execute' do - let(:finder) { described_class.new(params) } - let(:params) { {} } - - subject { finder.execute } - - context 'with params' do - let_it_be(:project) { create(:project) } - let_it_be(:project_metric) { create(:prometheus_metric, project: project) } - let_it_be(:common_metric) { create(:prometheus_metric, :common) } - let_it_be(:unique_metric) do - create( - :prometheus_metric, - :common, - title: 'Unique title', - y_label: 'Unique y_label', - group: :kubernetes, - identifier: 'identifier', - created_at: 5.minutes.ago - ) - end - - context 'with appropriate indexes' do - before do - allow_any_instance_of(described_class).to receive(:appropriate_index?).and_return(true) - end - - context 'with project' do - let(:params) { { project: project } } - - it { is_expected.to eq([project_metric]) } - end - - context 'with group' do - let(:params) { { group: project_metric.group } } - - it { is_expected.to contain_exactly(common_metric, project_metric) } - end - - context 'with title' do - let(:params) { { title: project_metric.title } } - - it { is_expected.to contain_exactly(project_metric, common_metric) } - end - - context 'with y_label' do - let(:params) { { y_label: project_metric.y_label } } - - it { is_expected.to contain_exactly(project_metric, common_metric) } - end - - context 'with common' do - let(:params) { { common: true } } - - it { is_expected.to contain_exactly(common_metric, unique_metric) } - end - - context 'with ordered' do - let(:params) { { ordered: true } } - - it { is_expected.to eq([unique_metric, project_metric, common_metric]) } - end - - context 'with indentifier' do - let(:params) { { identifier: unique_metric.identifier } } - - it 'raises an error' do - expect { subject }.to raise_error( - ArgumentError, - ':identifier must be scoped to a :project or :common' - ) - end - - context 'with common' do - let(:params) { { identifier: unique_metric.identifier, common: true } } - - it { is_expected.to contain_exactly(unique_metric) } - end - - context 'with id' do - let(:params) { { id: 14, identifier: 'string' } } - - it 'raises an error' do - expect { subject }.to raise_error( - ArgumentError, - 'Only one of :identifier, :id is permitted' - ) - end - end - end - - context 'with id' do - let(:params) { { id: common_metric.id } } - - it { is_expected.to contain_exactly(common_metric) } - end - - context 'with multiple params' do - let(:params) do - { - group: project_metric.group, - title: project_metric.title, - y_label: project_metric.y_label, - common: true, - ordered: true - } - end - - it { is_expected.to contain_exactly(common_metric) } - end - end - - context 'without an appropriate index' do - let(:params) do - { - title: project_metric.title, - ordered: true - } - end - - it 'raises an error' do - expect { subject }.to raise_error( - ArgumentError, - 'An index should exist for params: [:title]' - ) - end - end - end - - context 'without params' do - it 'raises an error' do - expect { subject }.to raise_error( - ArgumentError, - 'Please provide one or more of: [:project, :group, :title, :y_label, :identifier, :id, :common, :ordered]' - ) - end - end - end -end diff --git a/spec/frontend/members/placeholders/components/app_spec.js b/spec/frontend/members/placeholders/components/app_spec.js index a9264a69cee..6cb3a3dd623 100644 --- a/spec/frontend/members/placeholders/components/app_spec.js +++ b/spec/frontend/members/placeholders/components/app_spec.js @@ -7,12 +7,18 @@ import { GlTab, GlTabs } from '@gitlab/ui'; import { createAlert } from '~/alert'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import setWindowLocation from 'helpers/set_window_location_helper'; import PlaceholdersTabApp from '~/members/placeholders/components/app.vue'; import PlaceholdersTable from '~/members/placeholders/components/placeholders_table.vue'; import importSourceUsersQuery from '~/members/placeholders/graphql/queries/import_source_users.query.graphql'; import { MEMBERS_TAB_TYPES } from '~/members/constants'; -import { mockSourceUsersQueryResponse, mockSourceUsers, pagination } from '../mock_data'; +import { + mockSourceUsersQueryResponse, + mockSourceUsersFailedStatusResponse, + mockSourceUsers, + pagination, +} from '../mock_data'; Vue.use(Vuex); Vue.use(VueApollo); @@ -140,6 +146,7 @@ describe('PlaceholdersTabApp', () => { before: null, fullPath: mockGroup.path, first: 20, + statuses: [], }); }); @@ -208,4 +215,39 @@ describe('PlaceholdersTabApp', () => { }); }); }); + + describe('correctly filters users', () => { + const sourceUsersFailureQueryHandler = jest + .fn() + .mockResolvedValue(mockSourceUsersFailedStatusResponse); + + beforeEach(async () => { + setWindowLocation('?status=failed'); + + createComponent({ queryHandler: sourceUsersFailureQueryHandler }); + await waitForPromises(); + }); + + it('when the url includes the query param failed', () => { + const sourceUsersWithFailedStatus = + mockSourceUsersFailedStatusResponse.data.namespace.importSourceUsers; + const tableProps = findPlaceholdersTable().props(); + + expect(findPlaceholdersTable().props()).toMatchObject({ + isLoading: false, + items: sourceUsersWithFailedStatus.nodes, + pageInfo: sourceUsersWithFailedStatus.pageInfo, + }); + expect(tableProps.items.length).toBe(1); + expect(tableProps.items[0].status).toBe('FAILED'); + expect(sourceUsersFailureQueryHandler).toHaveBeenCalledTimes(1); + expect(sourceUsersFailureQueryHandler).toHaveBeenCalledWith({ + after: null, + before: null, + fullPath: mockGroup.path, + first: 20, + statuses: ['FAILED'], + }); + }); + }); }); diff --git a/spec/frontend/members/placeholders/mock_data.js b/spec/frontend/members/placeholders/mock_data.js index 6089b4a0d87..cc4d209fe72 100644 --- a/spec/frontend/members/placeholders/mock_data.js +++ b/spec/frontend/members/placeholders/mock_data.js @@ -84,6 +84,26 @@ export const mockSourceUsersQueryResponse = ({ pageInfo = {} } = {}) => ({ }, }); +export const mockSourceUsersFailedStatusResponse = { + data: { + namespace: { + __typename: 'Namespace', + id: 'gid://gitlab/Group/1', + importSourceUsers: { + __typename: 'ImportSourceUserConnection', + nodes: [mockSourceUsers[4]], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', + }, + }, + }, + }, +}; + export const mockReassignMutationResponse = { data: { importSourceUserReassign: { diff --git a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js index b2c03352cdc..0d473fa7962 100644 --- a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js +++ b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js @@ -8,12 +8,14 @@ const mockTopics = [ { id: 2, name: 'GitLab', title: 'GitLab', avatarUrl: 'avatar.com/GitLab.png' }, ]; +const USER_DEFINED_TOKEN = 'user defined token'; + describe('TopicsTokenSelector', () => { let wrapper; let div; let input; - const createComponent = (selected) => { + const createComponent = ({ selected, topics = mockTopics } = {}) => { wrapper = mount(TopicsTokenSelector, { attachTo: div, propsData: { @@ -21,7 +23,7 @@ describe('TopicsTokenSelector', () => { }, data() { return { - topics: mockTopics, + topics, }; }, mocks: { @@ -40,6 +42,9 @@ describe('TopicsTokenSelector', () => { const findAllAvatars = () => wrapper.findAllComponents(GlAvatarLabeled).wrappers; + const findSelectedTokensText = () => + wrapper.findAllComponents(GlToken).wrappers.map((w) => w.text()); + const setTokenSelectorInputValue = (value) => { const tokenSelectorInput = findTokenSelectorInput(); @@ -75,7 +80,7 @@ describe('TopicsTokenSelector', () => { { id: 12, name: 'topic2' }, { id: 13, name: 'topic3' }, ]; - createComponent(selected); + createComponent({ selected }); await nextTick(); wrapper.findAllComponents(GlToken).wrappers.forEach((tokenWrapper, index) => { @@ -103,4 +108,58 @@ describe('TopicsTokenSelector', () => { expect(event.preventDefault).toHaveBeenCalled(); }); }); + + describe('when tokens are added', () => { + it('properly updates selectedTokens and emits `update` with existing token', async () => { + createComponent(); + + await setTokenSelectorInputValue(mockTopics[0].name); + await tokenSelectorTriggerEnter(); + + expect(findSelectedTokensText()).toStrictEqual([mockTopics[0].name]); + expect(wrapper.emitted('update')[0][0]).toStrictEqual([mockTopics[0]]); + }); + + it('properly updates selectedTokens and emits `update` with user defined token', async () => { + createComponent({ topics: [] }); + + await setTokenSelectorInputValue(USER_DEFINED_TOKEN); + await tokenSelectorTriggerEnter(); + + expect(findSelectedTokensText()).toStrictEqual([USER_DEFINED_TOKEN]); + expect(wrapper.emitted('update')[0][0]).toStrictEqual([ + expect.objectContaining({ name: USER_DEFINED_TOKEN }), + ]); + }); + + it('properly omits duplicate tokens, updates selectedTokens, and emits `update`', async () => { + createComponent({ selected: mockTopics }); + + await setTokenSelectorInputValue(USER_DEFINED_TOKEN); + await tokenSelectorTriggerEnter(); + + expect(findSelectedTokensText()).toStrictEqual([ + mockTopics[0].name, + mockTopics[1].name, + USER_DEFINED_TOKEN, + ]); + expect(wrapper.emitted('update')[0][0]).toStrictEqual([ + ...mockTopics, + expect.objectContaining({ name: USER_DEFINED_TOKEN }), + ]); + + await setTokenSelectorInputValue(USER_DEFINED_TOKEN); + await tokenSelectorTriggerEnter(); + + expect(findSelectedTokensText()).toStrictEqual([ + mockTopics[0].name, + mockTopics[1].name, + USER_DEFINED_TOKEN, + ]); + expect(wrapper.emitted('update')[0][0]).toStrictEqual([ + ...mockTopics, + expect.objectContaining({ name: USER_DEFINED_TOKEN }), + ]); + }); + }); }); diff --git a/spec/lib/gitlab/background_migration/nullify_organization_id_for_snippets_spec.rb b/spec/lib/gitlab/background_migration/nullify_organization_id_for_snippets_spec.rb new file mode 100644 index 00000000000..8eebb45ad0b --- /dev/null +++ b/spec/lib/gitlab/background_migration/nullify_organization_id_for_snippets_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::NullifyOrganizationIdForSnippets, feature_category: :source_code_management do + let(:snippets) { table(:snippets) } + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') } + let!(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) } + + let!(:personal_snippet) do + snippets.create!( + type: 'PersonalSnippet', author_id: 1, project_id: nil, title: 'Snippet1', organization_id: 1 + ) + end + + let!(:project_snippet_with_organization) do + snippets.create!( + type: 'ProjectSnippet', author_id: 1, project_id: project.id, title: 'Snippet2', organization_id: 1 + ) + end + + let!(:project_snippet_without_organization) do + snippets.create!( + type: 'ProjectSnippet', author_id: 1, project_id: project.id, title: 'Snippet3', organization_id: nil + ) + end + + let(:migration_attrs) do + { + start_id: snippets.minimum(:id), + end_id: snippets.maximum(:id), + batch_table: :snippets, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: ApplicationRecord.connection + } + end + + it 'nullfies organization_id for project snippets' do + expect do + described_class.new(**migration_attrs).perform + end.to change { project_snippet_with_organization.reload.organization_id }.from(1).to(nil) + .and not_change { personal_snippet.reload.organization_id }.from(1) + .and not_change { project_snippet_without_organization.reload.organization_id }.from(nil) + end +end diff --git a/spec/migrations/20240726081618_queue_nullify_organization_id_for_snippets_spec.rb b/spec/migrations/20240726081618_queue_nullify_organization_id_for_snippets_spec.rb new file mode 100644 index 00000000000..bf50b2e064a --- /dev/null +++ b/spec/migrations/20240726081618_queue_nullify_organization_id_for_snippets_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueNullifyOrganizationIdForSnippets, feature_category: :source_code_management do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :snippets, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + } + end + end +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 44b438305a9..dda06145b86 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1368,6 +1368,32 @@ RSpec.describe Group, feature_category: :groups_and_projects do it { is_expected.to match_array(groups) } end + describe '.by_min_access_level' do + let_it_be(:user) { create(:user) } + let_it_be(:group1) { create(:group) } + let_it_be(:group2) { create(:group) } + + let(:owner_access_level) { Gitlab::Access::OWNER } + let(:developer_access_level) { Gitlab::Access::DEVELOPER } + + before do + create(:group_member, user: user, group: group1, access_level: owner_access_level) + create(:group_member, user: user, group: group2, access_level: developer_access_level) + end + + it 'returns groups where the user has the specified access level' do + result = described_class.by_min_access_level(user, owner_access_level) + + expect(result).to contain_exactly(group1) + end + + it 'returns groups if the user has greater or equal specified access level' do + result = described_class.by_min_access_level(user, developer_access_level) + + expect(result).to contain_exactly(group1, group2) + end + end + describe 'descendants_with_shared_with_groups' do subject { described_class.descendants_with_shared_with_groups(parent_group) } diff --git a/spec/models/packages/rpm/repository_file_spec.rb b/spec/models/packages/rpm/repository_file_spec.rb index 1147fd66ac6..b161ebe0dbb 100644 --- a/spec/models/packages/rpm/repository_file_spec.rb +++ b/spec/models/packages/rpm/repository_file_spec.rb @@ -17,7 +17,7 @@ RSpec.describe Packages::Rpm::RepositoryFile, type: :model, feature_category: :p end describe '.has_oversized_filelists?' do - let_it_be(:filelists) { create(:rpm_repository_file, :filelists, size: 21.megabytes) } + let!(:filelists) { create(:rpm_repository_file, :filelists, size: 21.megabytes) } subject { described_class.has_oversized_filelists?(project_id: filelists.project_id) } diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 282d145a67c..5e259a2ffa4 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -1928,7 +1928,7 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do it_behaves_like 'rate limited endpoint', rate_limit_key: :group_shared_groups_api do def request - get api("/groups/#{main_group.id}/groups/shared") + get api(path) end end @@ -1940,7 +1940,7 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do it_behaves_like 'unthrottled endpoint' def request - get api("/groups/#{main_group.id}/groups/shared") + get api(path) end end @@ -1973,8 +1973,6 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do let_it_be(:shared_group_1) { create(:group, :public, owners: user1) } let_it_be(:shared_group_2) { create(:group, :private, owners: user1) } - let(:path) { "/groups/#{main_group.id}/groups/shared" } - before do create(:group_group_link, shared_group: shared_group_1, shared_with_group: main_group) create(:group_group_link, shared_group: shared_group_2, shared_with_group: main_group) @@ -1991,6 +1989,70 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do end end + context "when using skip_groups in request" do + it "returns all shared groups excluding skipped groups", :aggregate_failures do + get api(path, user1), params: { skip_groups: [shared_group1.id] } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.map { |group| group['id'] }).to contain_exactly(shared_group2.id, other_group.id) + end + end + + context "when search is present in request" do + let_it_be(:new_shared_group) { create(:group, :public, name: "new search group", owners: user1) } + let_it_be(:other_shared_group) { create(:group, :private, name: "other group", owners: user1) } + + before do + create(:group_group_link, shared_group: new_shared_group, shared_with_group: main_group) + create(:group_group_link, shared_group: other_shared_group, shared_with_group: main_group) + end + + it 'filters the shared projects in the group based on search params', :aggregate_failures do + get api(path, user1), params: { search: 'new' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(new_shared_group.id) + end + end + + context 'when using min_access_level in the request' do + let_it_be(:new_main_group) do + create(:group, :private, owners: user1) + end + + let_it_be(:shared_group1) do + create(:group, :private) + end + + let_it_be(:shared_group2) do + create(:group, :private) + end + + before do + shared_group1.add_developer(user1) + shared_group2.add_reporter(user1) + create(:group_group_link, shared_group: shared_group1, shared_with_group: new_main_group) + create(:group_group_link, shared_group: shared_group2, shared_with_group: new_main_group) + end + + context 'with min_access_level parameter' do + it 'returns an array of groups the user has at least reporter access', :aggregate_failures do + get api("/groups/#{new_main_group.id}/groups/shared", user1), params: { min_access_level: Gitlab::Access::REPORTER } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |group| group['id'] }).to contain_exactly(shared_group1.id, shared_group2.id) + end + end + end + context "when using sorting" do let(:response_groups) { json_response.map { |group| group['name'] } } let(:response_group_paths) { json_response.map { |group| group['path'] } } @@ -2000,7 +2062,7 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do let(:shared_group_ids) { [shared_group1.id, shared_group2.id, other_group.id] } it "sorts by name ascending by default", :aggregate_failures do - get api("/groups/#{main_group.id}/groups/shared", user1) + get api(path, user1) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2010,7 +2072,7 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do end it "sorts in descending order when passed", :aggregate_failures do - get api("/groups/#{main_group.id}/groups/shared", user1), params: { sort: "desc" } + get api(path, user1), params: { sort: "desc" } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2020,7 +2082,7 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do end it "sorts by path in order_by param", :aggregate_failures do - get api("/groups/#{main_group.id}/groups/shared", user1), params: { order_by: "path" } + get api(path, user1), params: { order_by: "path" } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2029,7 +2091,7 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do end it "sorts by id in the order_by param", :aggregate_failures do - get api("/groups/#{main_group.id}/groups/shared", user1), params: { order_by: "id" } + get api(path, user1), params: { order_by: "id" } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2062,8 +2124,8 @@ RSpec.describe API::Groups, feature_category: :groups_and_projects do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response.length).to eq(3) - expect(response_groups).to eq(['same-name shared', 'same-name shared_other', 'other-name']) + expect(json_response.length).to eq(2) + expect(response_groups).to eq(['same-name shared', 'same-name shared_other']) end context 'when `search` parameter is not given' do diff --git a/spec/requests/sandbox_controller_spec.rb b/spec/requests/sandbox_controller_spec.rb index 26a7422680c..ae4b31f0318 100644 --- a/spec/requests/sandbox_controller_spec.rb +++ b/spec/requests/sandbox_controller_spec.rb @@ -4,11 +4,39 @@ require 'spec_helper' RSpec.describe SandboxController, feature_category: :shared do describe 'GET #mermaid' do + subject(:get_mermaid) { get sandbox_mermaid_path } + it 'renders page without template' do - get sandbox_mermaid_path + get_mermaid expect(response).to have_gitlab_http_status(:ok) expect(response).to render_template(layout: nil) end + + context 'with a signed-in user' do + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'renders page' do + get_mermaid + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when enforce_terms setting is enabled' do + before do + stub_application_setting(enforce_terms: true) + end + + it 'does not enforce terms for rendering Mermaid markdown' do + get_mermaid + + expect(response).to have_gitlab_http_status(:ok) + end + end + end end end diff --git a/spec/services/personal_access_tokens/last_used_service_spec.rb b/spec/services/personal_access_tokens/last_used_service_spec.rb index 22c52d6a5af..b31cc4dd754 100644 --- a/spec/services/personal_access_tokens/last_used_service_spec.rb +++ b/spec/services/personal_access_tokens/last_used_service_spec.rb @@ -46,25 +46,6 @@ RSpec.describe PersonalAccessTokens::LastUsedService, feature_category: :system_ end end - context 'when use_lease_for_pat_last_used_update flag is disabled' do - before do - stub_feature_flags(use_lease_for_pat_last_used_update: false) - end - - it 'does not obtain an exclusive lease before updating' do - Gitlab::Redis::SharedState.with do |redis| - expect(redis).not_to receive(:set).with( - "#{Gitlab::ExclusiveLease::PREFIX}:pat:last_used_update_lock:#{personal_access_token.id}", - anything, - nx: true, - ex: described_class::LEASE_TIMEOUT - ) - end - - expect { subject }.to change { personal_access_token.last_used_at } - end - end - context 'when database load balancing is configured' do let!(:service) { described_class.new(personal_access_token) } diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 844273ec600..969f71e4862 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -3790,9 +3790,7 @@ - './spec/finders/projects/groups_finder_spec.rb' - './spec/finders/projects/members/effective_access_level_finder_spec.rb' - './spec/finders/projects/members/effective_access_level_per_user_finder_spec.rb' -- './spec/finders/projects/prometheus/alerts_finder_spec.rb' - './spec/finders/projects/topics_finder_spec.rb' -- './spec/finders/prometheus_metrics_finder_spec.rb' - './spec/finders/protected_branches_finder_spec.rb' - './spec/finders/releases/evidence_pipeline_finder_spec.rb' - './spec/finders/releases_finder_spec.rb'