From eaaf34e0422666c072b980ec6e9f0100d1937ddf Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 8 Aug 2024 15:11:00 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .eslintrc.yml | 8 + .gitlab/ci/qa-common/variables.gitlab-ci.yml | 2 +- .../factory_bot/strategy_in_callback.yml | 3 +- .../clusters_list/components/agent_table.vue | 4 +- .../clusters_list/components/agents.vue | 16 +- .../components/delete_agent_button.vue | 1 - .../fragments/cluster_agent.fragment.graphql | 4 + .../graphql/queries/get_agents.query.graphql | 15 +- .../queries/get_tree_list.query.graphql | 18 ++ .../kubernetes/kubernetes_overview.vue | 4 +- .../environment_cluster_agent.query.graphql | 4 + .../application_controller.rb | 8 +- .../import/source_users_controller.rb | 4 +- app/models/concerns/packages/installable.rb | 4 +- app/models/import/source_user.rb | 1 + .../source_user_placeholder_reference.rb | 65 +++++ app/models/packages/composer/package.rb | 2 - app/models/packages/conan/package.rb | 2 - app/models/packages/debian/package.rb | 2 - app/models/packages/package.rb | 4 + app/models/packages/package_file.rb | 4 + app/models/packages/rpm/package.rb | 2 - app/models/packages/rpm/repository_file.rb | 4 + ...assign_placeholder_user_records_service.rb | 65 +++++ .../accept_reassignment_service.rb | 31 ++ app/workers/all_queues.yml | 9 + ...eassign_placeholder_user_records_worker.rb | 63 ++++ .../feature_flags/beta/rewrite_history_ui.yml | 2 +- config/sidekiq_queues.yml | 2 + .../alter_webhook_deleted_audit_event.yml | 9 + ...queue_alter_webhook_deleted_audit_event.rb | 26 ++ db/schema_migrations/20240729133817 | 1 + doc/development/cells/topology_service.md | 26 ++ doc/development/documentation/help.md | 17 +- doc/topics/git/undo.md | 1 + doc/user/gitlab_duo/turn_on_off.md | 27 ++ .../reducing_the_repo_size_using_git.md | 1 + .../alter_webhook_deleted_audit_event.rb | 20 ++ locale/gitlab.pot | 22 +- scripts/update-topology-service-gem.sh | 51 ++++ ...cy_proxy_for_containers_controller_spec.rb | 16 +- spec/factories/import_source_users.rb | 5 + spec/factories/project_members.rb | 6 - .../import/source_users_finder_spec.rb | 5 +- .../components/agent_table_spec.js | 1 - .../clusters_list/components/agents_spec.js | 39 ++- .../clusters_list/components/mock_data.js | 30 ++ spec/frontend/clusters_list/mocks/apollo.js | 4 + .../kubernetes/kubernetes_overview_spec.js | 2 +- .../environments/graphql/mock_data.js | 1 + .../alter_webhook_deleted_audit_event_spec.rb | 56 ++++ ...tics_storage_size_with_recent_size_spec.rb | 1 + ..._alter_webhook_deleted_audit_event_spec.rb | 26 ++ .../source_user_placeholder_reference_spec.rb | 236 +++++++++++++++ spec/models/import/source_user_spec.rb | 21 +- spec/models/packages/composer/package_spec.rb | 4 + spec/models/packages/conan/package_spec.rb | 4 + spec/models/packages/debian/package_spec.rb | 4 + spec/models/packages/go/package_spec.rb | 4 + spec/models/packages/ml_model/package_spec.rb | 4 + spec/models/packages/package_file_spec.rb | 4 + spec/models/packages/package_spec.rb | 16 +- spec/models/packages/rpm/package_spec.rb | 4 + .../packages/rpm/repository_file_spec.rb | 21 +- spec/models/packages/rubygems/package_spec.rb | 4 + .../import/source_users_controller_spec.rb | 6 + ...n_placeholder_user_records_service_spec.rb | 272 ++++++++++++++++++ .../accept_reassignment_service_spec.rb | 62 ++++ .../resend_notification_service_spec.rb | 2 +- .../lint_factories_shared_examples.rb | 8 +- .../packages/installable_shared_examples.rb | 28 ++ spec/workers/every_sidekiq_worker_spec.rb | 1 + ...gn_placeholder_user_records_worker_spec.rb | 83 ++++++ .../gitlab-topology-service-client/GITLAB.md | 6 +- .../gitlab-topology-service-client/REVISION | 1 + .../gitlab-topology-service-client.gemspec | 2 +- 76 files changed, 1456 insertions(+), 87 deletions(-) create mode 100644 app/assets/javascripts/clusters_list/graphql/queries/get_tree_list.query.graphql create mode 100644 app/services/import/reassign_placeholder_user_records_service.rb create mode 100644 app/services/import/source_users/accept_reassignment_service.rb create mode 100644 app/workers/import/reassign_placeholder_user_records_worker.rb create mode 100644 db/docs/batched_background_migrations/alter_webhook_deleted_audit_event.yml create mode 100644 db/post_migrate/20240729133817_queue_alter_webhook_deleted_audit_event.rb create mode 100644 db/schema_migrations/20240729133817 create mode 100644 doc/development/cells/topology_service.md create mode 100644 lib/gitlab/background_migration/alter_webhook_deleted_audit_event.rb create mode 100755 scripts/update-topology-service-gem.sh create mode 100644 spec/lib/gitlab/background_migration/alter_webhook_deleted_audit_event_spec.rb create mode 100644 spec/migrations/20240729133817_queue_alter_webhook_deleted_audit_event_spec.rb create mode 100644 spec/services/import/reassign_placeholder_user_records_service_spec.rb create mode 100644 spec/services/import/source_users/accept_reassignment_service_spec.rb create mode 100644 spec/support/shared_examples/packages/installable_shared_examples.rb create mode 100644 spec/workers/import/reassign_placeholder_user_records_worker_spec.rb create mode 100644 vendor/gems/gitlab-topology-service-client/REVISION diff --git a/.eslintrc.yml b/.eslintrc.yml index 14ac572fff1..bd5c6e25891 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -126,11 +126,19 @@ rules: message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`' - selector: TemplateLiteral[expressions.0.name=DOCS_URL] > TemplateElement[value.cooked=/\u002Fjh|\u002Fee/] message: '`/ee` or `/jh` path found in docs url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`' + - selector: "MemberExpression[object.type='ThisExpression'][property.name='$delete']" + message: "'this.$delete' is restricted from being used. $delete is not supported in Vue 3; see the migration documentation for more information" no-restricted-properties: - error - object: window property: open message: 'Use `visitUrl` in `jh_else_ce/lib/utils/url_utility` to avoid cross-site leaks.' + - object: vm + property: $delete + message: '$delete is not supported in Vue 3; see the migration documentation for more information' + - object: Vue + property: delete + message: 'delete is not supported in Vue 3; see the migration documentation for more information' no-restricted-imports: - error - paths: diff --git a/.gitlab/ci/qa-common/variables.gitlab-ci.yml b/.gitlab/ci/qa-common/variables.gitlab-ci.yml index 19d2ee75db2..1263a095a26 100644 --- a/.gitlab/ci/qa-common/variables.gitlab-ci.yml +++ b/.gitlab/ci/qa-common/variables.gitlab-ci.yml @@ -18,4 +18,4 @@ variables: # Retry failed specs in separate process QA_RETRY_FAILED_SPECS: "true" # helm chart ref used by test-on-cng pipeline - GITLAB_HELM_CHART_REF: "c8e9ac56ccda4640e8979c7896ffe9cca9e626ad" + GITLAB_HELM_CHART_REF: "5cfe64ff4d0e8fde3fab778dfd3f5c8b4b66ecb5" diff --git a/.rubocop_todo/rspec/factory_bot/strategy_in_callback.yml b/.rubocop_todo/rspec/factory_bot/strategy_in_callback.yml index 0575216dac5..cfa42cc4603 100644 --- a/.rubocop_todo/rspec/factory_bot/strategy_in_callback.yml +++ b/.rubocop_todo/rspec/factory_bot/strategy_in_callback.yml @@ -8,9 +8,11 @@ RSpec/FactoryBot/StrategyInCallback: - 'ee/spec/factories/elastic/reindexing_tasks.rb' - 'ee/spec/factories/epic_issues.rb' - 'ee/spec/factories/geo_nodes.rb' + - 'ee/spec/factories/group_members.rb' - 'ee/spec/factories/groups.rb' - 'ee/spec/factories/merge_requests.rb' - 'ee/spec/factories/namespaces.rb' + - 'ee/spec/factories/project_members.rb' - 'ee/spec/factories/projects.rb' - 'ee/spec/factories/protected_environments.rb' - 'ee/spec/factories/users.rb' @@ -42,7 +44,6 @@ RSpec/FactoryBot/StrategyInCallback: - 'spec/factories/packages/package_files.rb' - 'spec/factories/packages/packages.rb' - 'spec/factories/pool_repositories.rb' - - 'spec/factories/project_members.rb' - 'spec/factories/projects.rb' - 'spec/factories/releases.rb' - 'spec/factories/resource_label_events.rb' diff --git a/app/assets/javascripts/clusters_list/components/agent_table.vue b/app/assets/javascripts/clusters_list/components/agent_table.vue index b8dc4bb63f2..29b61ef2884 100644 --- a/app/assets/javascripts/clusters_list/components/agent_table.vue +++ b/app/assets/javascripts/clusters_list/components/agent_table.vue @@ -61,7 +61,7 @@ export default { configHelpLink: helpPagePath('user/clusters/agent/install/index', { anchor: 'create-an-agent-configuration-file', }), - inject: ['kasCheckVersion', 'projectPath'], + inject: ['kasCheckVersion'], props: { agents: { required: true, @@ -426,7 +426,7 @@ export default { diff --git a/app/assets/javascripts/clusters_list/components/agents.vue b/app/assets/javascripts/clusters_list/components/agents.vue index 39b6e287288..10ba736dd92 100644 --- a/app/assets/javascripts/clusters_list/components/agents.vue +++ b/app/assets/javascripts/clusters_list/components/agents.vue @@ -7,6 +7,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import { AGENT_FEEDBACK_ISSUE, AGENT_FEEDBACK_KEY } from '../constants'; import getAgentsQuery from '../graphql/queries/get_agents.query.graphql'; +import getTreeList from '../graphql/queries/get_tree_list.query.graphql'; import { getAgentLastContact, getAgentStatus } from '../clusters_util'; import AgentEmptyState from './agent_empty_state.vue'; import AgentTable from './agent_table.vue'; @@ -27,12 +28,10 @@ export default { query: getAgentsQuery, variables() { return { - defaultBranchName: this.defaultBranchName, projectPath: this.projectPath, }; }, update(data) { - this.updateTreeList(data); return data; }, result() { @@ -42,6 +41,19 @@ export default { this.queryErrored = true; }, }, + treeList: { + query: getTreeList, + variables() { + return { + defaultBranchName: this.defaultBranchName, + projectPath: this.projectPath, + }; + }, + update(data) { + this.updateTreeList(data); + return data; + }, + }, }, components: { AgentEmptyState, diff --git a/app/assets/javascripts/clusters_list/components/delete_agent_button.vue b/app/assets/javascripts/clusters_list/components/delete_agent_button.vue index ead87d93f22..d66e2974ade 100644 --- a/app/assets/javascripts/clusters_list/components/delete_agent_button.vue +++ b/app/assets/javascripts/clusters_list/components/delete_agent_button.vue @@ -59,7 +59,6 @@ export default { }, getAgentsQueryVariables() { return { - defaultBranchName: this.defaultBranchName, projectPath: this.projectPath, }; }, diff --git a/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql b/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql index 4174bbc29ed..0d9e11b69fb 100644 --- a/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql +++ b/app/assets/javascripts/clusters_list/graphql/fragments/cluster_agent.fragment.graphql @@ -3,6 +3,10 @@ fragment ClusterAgentFragment on ClusterAgent { name webPath createdAt + project { + id + fullPath + } userAccessAuthorizations { config } diff --git a/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql b/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql index 2a4f7b42eff..75da999be22 100644 --- a/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql +++ b/app/assets/javascripts/clusters_list/graphql/queries/get_agents.query.graphql @@ -1,6 +1,6 @@ #import "../fragments/cluster_agent.fragment.graphql" -query getAgents($defaultBranchName: String!, $projectPath: ID!) { +query getAgents($projectPath: ID!) { project(fullPath: $projectPath) { id clusterAgents { @@ -24,18 +24,5 @@ query getAgents($defaultBranchName: String!, $projectPath: ID!) { } } } - - repository { - tree(path: ".gitlab/agents", ref: $defaultBranchName) { - trees { - nodes { - id - name - path - webPath - } - } - } - } } } diff --git a/app/assets/javascripts/clusters_list/graphql/queries/get_tree_list.query.graphql b/app/assets/javascripts/clusters_list/graphql/queries/get_tree_list.query.graphql new file mode 100644 index 00000000000..2d55bb830f7 --- /dev/null +++ b/app/assets/javascripts/clusters_list/graphql/queries/get_tree_list.query.graphql @@ -0,0 +1,18 @@ +query getTreeList($defaultBranchName: String!, $projectPath: ID!) { + project(fullPath: $projectPath) { + id + + repository { + tree(path: ".gitlab/agents", ref: $defaultBranchName) { + trees { + nodes { + id + name + path + webPath + } + } + } + } + } +} diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue index 9ee340fdb6e..de389b44e56 100644 --- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue +++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue @@ -57,7 +57,7 @@ export default { directives: { GlModalDirective, }, - inject: ['kasTunnelUrl', 'projectPath'], + inject: ['kasTunnelUrl'], props: { environmentName: { type: String, @@ -268,7 +268,7 @@ export default { diff --git a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql index 04e919db546..c83d57871ec 100644 --- a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql +++ b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql @@ -10,6 +10,10 @@ query getEnvironmentClusterAgent($projectFullPath: ID!, $environmentName: String id name webPath + project { + id + fullPath + } tokens { nodes { id diff --git a/app/controllers/groups/dependency_proxy/application_controller.rb b/app/controllers/groups/dependency_proxy/application_controller.rb index 11a7ef3189d..0c83cccbe4b 100644 --- a/app/controllers/groups/dependency_proxy/application_controller.rb +++ b/app/controllers/groups/dependency_proxy/application_controller.rb @@ -63,7 +63,7 @@ module Groups case user_or_token when User @authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :user, []) - sign_in(user_or_token) + sign_in(user_or_token) if can_sign_in?(user_or_token) when PersonalAccessToken @authentication_result = Gitlab::Auth::Result.new(user_or_token.user, nil, :personal_access_token, []) @personal_access_token = user_or_token @@ -71,6 +71,12 @@ module Groups @authentication_result = Gitlab::Auth::Result.new(user_or_token, nil, :deploy_token, []) end end + + def can_sign_in?(user_or_token) + return false if user_or_token.project_bot? || user_or_token.service_account? + + true + end end end end diff --git a/app/controllers/import/source_users_controller.rb b/app/controllers/import/source_users_controller.rb index 97a94ef79dd..cf7dad7aeec 100644 --- a/app/controllers/import/source_users_controller.rb +++ b/app/controllers/import/source_users_controller.rb @@ -12,9 +12,9 @@ module Import feature_category :importers def accept - if source_user.accept - # TODO: This is where we enqueue the job to assign the contributions. + result = ::Import::SourceUsers::AcceptReassignmentService.new(source_user, current_user: current_user).execute + if result.status == :success flash[:raw] = banner('accept_invite') redirect_to(dashboard_groups_path) else diff --git a/app/models/concerns/packages/installable.rb b/app/models/concerns/packages/installable.rb index e9303e55412..f4ad7ae906d 100644 --- a/app/models/concerns/packages/installable.rb +++ b/app/models/concerns/packages/installable.rb @@ -2,7 +2,7 @@ module Packages # This module requires a status column. - # It also requires a constant INSTALLABLE_STATUSES. This should be + # It also requires a class method `installable_statuses`. This should be # an array that defines which values of the status column are # considered as installable. module Installable @@ -10,7 +10,7 @@ module Packages included do scope :with_status, ->(status) { where(status: status) } - scope :installable, -> { with_status(const_get(:INSTALLABLE_STATUSES, false)) } + scope :installable, -> { with_status(installable_statuses) } end end end diff --git a/app/models/import/source_user.rb b/app/models/import/source_user.rb index 29141ad4d91..93ce06528f8 100644 --- a/app/models/import/source_user.rb +++ b/app/models/import/source_user.rb @@ -20,6 +20,7 @@ module Import validates :namespace_id, :import_type, :source_hostname, :source_user_identifier, :status, presence: true validates :placeholder_user_id, presence: true, unless: :completed? + validates :reassign_to_user_id, presence: true, if: :reassignment_in_progress? scope :for_namespace, ->(namespace_id) { where(namespace_id: namespace_id) } scope :by_statuses, ->(statuses) { where(status: statuses) } diff --git a/app/models/import/source_user_placeholder_reference.rb b/app/models/import/source_user_placeholder_reference.rb index 4542e82f114..ab27dc7ff5e 100644 --- a/app/models/import/source_user_placeholder_reference.rb +++ b/app/models/import/source_user_placeholder_reference.rb @@ -3,6 +3,7 @@ module Import class SourceUserPlaceholderReference < ApplicationRecord include BulkInsertSafe + include EachBatch self.table_name = 'import_source_user_placeholder_references' @@ -18,6 +19,14 @@ module Import attribute :composite_key, :ind_jsonb + scope :model_groups_for_source_user, ->(source_user) do + where(source_user: source_user) + .select(:model, :user_reference_column) + .group(:model, :user_reference_column) + end + + MODEL_BATCH_LIMIT = 500 + # If an element is ever added to this array, ensure that `.from_serialized` handles receiving # older versions of the array by filling in missing values with defaults. We'd keep that in place # for at least one release cycle to ensure backward compatibility. @@ -50,6 +59,12 @@ module Import Gitlab::Json.dump(attributes.slice(*SERIALIZABLE_ATTRIBUTES).to_h.values) end + def model_record + model_class = model.constantize + model_relation = numeric_key? ? model_class.primary_key_in(numeric_key) : model_class.where(composite_key) + model_relation.where({ user_reference_column => source_user.placeholder_user_id }).first + end + class << self def from_serialized(serialized_reference) deserialized = Gitlab::Json.parse(serialized_reference) @@ -60,6 +75,56 @@ module Import new(attributes.merge(created_at: Time.zone.now)) end + + # Model relations are yielded in a block to ensure all relations will be batched, regardless of the model + def model_relations_for_source_user_reference(model:, source_user:, user_reference_column:) + # Look up model from alias after https://gitlab.com/gitlab-org/gitlab/-/issues/467522 + model = model.constantize + + where( + source_user: source_user, + model: model.to_s, + user_reference_column: user_reference_column + ).each_batch(of: MODEL_BATCH_LIMIT) do |placeholder_reference_batch| + primary_key = model.primary_key + model_relation = nil + + # This is the simplest way to check for composite pkey for now. In Rails 7.1, composite primary keys will be + # fully supported: https://guides.rubyonrails.org/7_1_release_notes.html#composite-primary-keys + # .pluck is used instead of .select to avoid CrossSchemaAccessErrors on CI tables + # rubocop: disable Database/AvoidUsingPluckWithoutLimit -- plucking limited by placeholder batch + if primary_key.nil? + composite_keys = placeholder_reference_batch.pluck(:composite_key) + + model_relation = model.where( + "#{composite_key_columns(composite_keys)} IN #{composite_key_values(composite_keys)}" + ) + else + model_relation = model.primary_key_in(placeholder_reference_batch.pluck(:numeric_key)) + end + # rubocop: enable Database/AvoidUsingPluckWithoutLimit + + model_relation = model_relation.where({ user_reference_column => source_user.placeholder_user_id }) + + next if model_relation.empty? + + yield([model_relation, placeholder_reference_batch]) + end + end + + def composite_key_columns(composite_keys) + composite_key_columns = composite_keys.first.keys + tuple(composite_key_columns) + end + + def composite_key_values(composite_keys) + keys = composite_keys.map { |composite_key| tuple(composite_key.values) } + tuple(keys) + end + + def tuple(values) + "(#{values.join(', ')})" + end end private diff --git a/app/models/packages/composer/package.rb b/app/models/packages/composer/package.rb index e363784146d..05b41a8317e 100644 --- a/app/models/packages/composer/package.rb +++ b/app/models/packages/composer/package.rb @@ -5,8 +5,6 @@ module Packages class Package < ::Packages::Package self.allow_legacy_sti_class = true - INSTALLABLE_STATUSES = ::Packages::Package::INSTALLABLE_STATUSES - has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum' delegate :target_sha, to: :composer_metadatum diff --git a/app/models/packages/conan/package.rb b/app/models/packages/conan/package.rb index a5bb83f71de..bd64c116cbf 100644 --- a/app/models/packages/conan/package.rb +++ b/app/models/packages/conan/package.rb @@ -5,8 +5,6 @@ module Packages class Package < Packages::Package self.allow_legacy_sti_class = true - INSTALLABLE_STATUSES = %i[default hidden].freeze - has_one :conan_metadatum, inverse_of: :package, class_name: 'Packages::Conan::Metadatum' accepts_nested_attributes_for :conan_metadatum diff --git a/app/models/packages/debian/package.rb b/app/models/packages/debian/package.rb index ab36842ad6c..d7c56dccbb9 100644 --- a/app/models/packages/debian/package.rb +++ b/app/models/packages/debian/package.rb @@ -3,8 +3,6 @@ module Packages module Debian class Package < Packages::Package - INSTALLABLE_STATUSES = [:default, :hidden].freeze - self.allow_legacy_sti_class = true has_one :publication, inverse_of: :package, class_name: 'Packages::Debian::Publication' diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 47c00ff75f9..3e8e0bf3e43 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -252,6 +252,10 @@ class Packages::Package < ApplicationRecord end end + def self.installable_statuses + INSTALLABLE_STATUSES + end + def versions project.packages .preload_pipelines diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb index f695eda308b..d37a4753915 100644 --- a/app/models/packages/package_file.rb +++ b/app/models/packages/package_file.rb @@ -133,6 +133,10 @@ class Packages::PackageFile < ApplicationRecord query.with(cte.to_arel) end + def self.installable_statuses + INSTALLABLE_STATUSES + end + def download_path Gitlab::Routing.url_helpers.download_project_package_file_path(project, self) end diff --git a/app/models/packages/rpm/package.rb b/app/models/packages/rpm/package.rb index fe1ef0893c9..a6c015b3a28 100644 --- a/app/models/packages/rpm/package.rb +++ b/app/models/packages/rpm/package.rb @@ -5,8 +5,6 @@ module Packages class Package < ::Packages::Package self.allow_legacy_sti_class = true - INSTALLABLE_STATUSES = %i[default hidden].freeze - has_one :rpm_metadatum, inverse_of: :package, class_name: 'Packages::Rpm::Metadatum' end end diff --git a/app/models/packages/rpm/repository_file.rb b/app/models/packages/rpm/repository_file.rb index bbd435691d2..f6ded66cf41 100644 --- a/app/models/packages/rpm/repository_file.rb +++ b/app/models/packages/rpm/repository_file.rb @@ -30,6 +30,10 @@ module Packages size: [FILELISTS_SIZE_LIMITATION..] ).exists? end + + def self.installable_statuses + INSTALLABLE_STATUSES + end end end end diff --git a/app/services/import/reassign_placeholder_user_records_service.rb b/app/services/import/reassign_placeholder_user_records_service.rb new file mode 100644 index 00000000000..3e34f89a181 --- /dev/null +++ b/app/services/import/reassign_placeholder_user_records_service.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Import + class ReassignPlaceholderUserRecordsService + NoReassignToUser = Class.new(StandardError) + + attr_accessor :import_source_user + + def initialize(import_source_user) + @import_source_user = import_source_user + end + + def execute + return unless import_source_user.reassignment_in_progress? + + Import::SourceUserPlaceholderReference.model_groups_for_source_user(import_source_user).each do |reference_group| + model = reference_group.model + user_reference_column = reference_group.user_reference_column + + begin + Import::SourceUserPlaceholderReference.model_relations_for_source_user_reference( + model: model, + source_user: import_source_user, + user_reference_column: user_reference_column + ) do |model_relation, placeholder_references| + reassign_placeholder_records_batch(model_relation, placeholder_references, user_reference_column) + end + rescue NameError => e + ::Import::Framework::Logger.error( + message: "#{model} is not a model, #{user_reference_column} cannot be reassigned.", + error: e.message, + source_user_id: import_source_user&.id + ) + + next + end + end + + import_source_user.complete! + end + + private + + def reassign_placeholder_records_batch(model_relation, placeholder_references, user_reference_column) + ApplicationRecord.transaction do + model_relation.update_all({ user_reference_column => import_source_user.reassign_to_user_id }) + placeholder_references.delete_all + end + rescue ActiveRecord::RecordNotUnique + placeholder_references.each do |placeholder_reference| + reassign_placeholder_record(placeholder_reference, user_reference_column) + end + end + + def reassign_placeholder_record(placeholder_reference, user_reference_column) + placeholder_reference.model_record.update!({ user_reference_column => import_source_user.reassign_to_user_id }) + placeholder_reference.destroy! + rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid + ::Import::Framework::Logger.warn( + message: "Unable to reassign record, reassigned user is invalid or not unique", + source_user_id: import_source_user.id + ) + end + end +end diff --git a/app/services/import/source_users/accept_reassignment_service.rb b/app/services/import/source_users/accept_reassignment_service.rb new file mode 100644 index 00000000000..f0bbab94c35 --- /dev/null +++ b/app/services/import/source_users/accept_reassignment_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Import + module SourceUsers + class AcceptReassignmentService < BaseService + def initialize(import_source_user, current_user:) + @import_source_user = import_source_user + @current_user = current_user + end + + def execute + return error_invalid_permissions unless current_user_matches_reassign_to_user + + if import_source_user.accept + Import::ReassignPlaceholderUserRecordsWorker.perform_async(import_source_user.id) + ServiceResponse.success(payload: import_source_user) + else + ServiceResponse.error(payload: import_source_user, message: import_source_user.errors.full_messages) + end + end + + private + + def current_user_matches_reassign_to_user + return false if current_user.nil? + + current_user.id == import_source_user.reassign_to_user_id + end + end + end +end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index f716682eb8c..14f0f414e3f 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -3261,6 +3261,15 @@ :weight: 1 :idempotent: true :tags: [] +- :name: import_reassign_placeholder_user_records + :worker_name: Import::ReassignPlaceholderUserRecordsWorker + :feature_category: :importers + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: import_refresh_import_jid :worker_name: Gitlab::Import::RefreshImportJidWorker :feature_category: :importers diff --git a/app/workers/import/reassign_placeholder_user_records_worker.rb b/app/workers/import/reassign_placeholder_user_records_worker.rb new file mode 100644 index 00000000000..44a512e5994 --- /dev/null +++ b/app/workers/import/reassign_placeholder_user_records_worker.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Import + class ReassignPlaceholderUserRecordsWorker + include ApplicationWorker + include Gitlab::Utils::StrongMemoize + + idempotent! + data_consistency :sticky + feature_category :importers + deduplicate :until_executed + sidekiq_options retry: 5, dead: false + sidekiq_options max_retries_after_interruption: 20 + + sidekiq_retries_exhausted do |msg, exception| + new.perform_failure(exception, msg['args']) + end + + def perform(import_source_user_id, _params = {}) + @import_source_user = Import::SourceUser.find_by_id(import_source_user_id) + + return unless Feature.enabled?( + :importer_user_mapping, + User.actor_from_id(import_source_user&.reassigned_by_user_id) + ) + + return unless import_source_user_valid? + + Import::ReassignPlaceholderUserRecordsService.new(import_source_user).execute + end + + def perform_failure(exception, import_source_user_id) + @import_source_user = Import::SourceUser.find_by_id(import_source_user_id) + + log_and_fail_reassignment(exception) + end + + private + + attr_reader :import_source_user + + def import_source_user_valid? + return true if import_source_user && import_source_user.reassignment_in_progress? + + ::Import::Framework::Logger.warn( + message: 'Unable to begin reassignment because Import source user has an invalid status or does not exist', + source_user_id: import_source_user&.id + ) + + false + end + + def log_and_fail_reassignment(exception) + ::Import::Framework::Logger.error( + message: 'Failed to reassign placeholder user', + error: exception.message, + source_user_id: import_source_user&.id + ) + + import_source_user.fail_reassignment! + end + end +end diff --git a/config/feature_flags/beta/rewrite_history_ui.yml b/config/feature_flags/beta/rewrite_history_ui.yml index 39352861110..41cfb56af6a 100644 --- a/config/feature_flags/beta/rewrite_history_ui.yml +++ b/config/feature_flags/beta/rewrite_history_ui.yml @@ -6,4 +6,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/462999 milestone: '17.1' group: group::source code type: beta -default_enabled: false +default_enabled: true diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 762b78abaed..d86f1595747 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -407,6 +407,8 @@ - 2 - - import_load_placeholder_references - 1 +- - import_reassign_placeholder_user_records + - 1 - - import_refresh_import_jid - 1 - - incident_management diff --git a/db/docs/batched_background_migrations/alter_webhook_deleted_audit_event.yml b/db/docs/batched_background_migrations/alter_webhook_deleted_audit_event.yml new file mode 100644 index 00000000000..122fd6efd67 --- /dev/null +++ b/db/docs/batched_background_migrations/alter_webhook_deleted_audit_event.yml @@ -0,0 +1,9 @@ +--- +migration_job_name: AlterWebhookDeletedAuditEvent +description: alters existing data for specific audit events +feature_category: webhooks +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161237 +milestone: '17.3' +queued_migration_version: 20240729133817 +finalize_after: '2024-08-22' +finalized_by: # version of the migration that finalized this BBM diff --git a/db/post_migrate/20240729133817_queue_alter_webhook_deleted_audit_event.rb b/db/post_migrate/20240729133817_queue_alter_webhook_deleted_audit_event.rb new file mode 100644 index 00000000000..0e54ad633ef --- /dev/null +++ b/db/post_migrate/20240729133817_queue_alter_webhook_deleted_audit_event.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class QueueAlterWebhookDeletedAuditEvent < Gitlab::Database::Migration[2.2] + milestone '17.3' + restrict_gitlab_migration gitlab_schema: :gitlab_main + + MIGRATION = "AlterWebhookDeletedAuditEvent" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 1000 + SUB_BATCH_SIZE = 100 + + def up + queue_batched_background_migration( + MIGRATION, + :audit_events, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :audit_events, :id, []) + end +end diff --git a/db/schema_migrations/20240729133817 b/db/schema_migrations/20240729133817 new file mode 100644 index 00000000000..36e881f9e0d --- /dev/null +++ b/db/schema_migrations/20240729133817 @@ -0,0 +1 @@ +1b97d718c83c46dce5429321e7d6ab307a623fd4b22eda6900a21e451b5fdb99 \ No newline at end of file diff --git a/doc/development/cells/topology_service.md b/doc/development/cells/topology_service.md new file mode 100644 index 00000000000..22b67db8674 --- /dev/null +++ b/doc/development/cells/topology_service.md @@ -0,0 +1,26 @@ +--- +stage: Data Stores +group: Tenant Scale +info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review. +--- + +# Topology Service + +## Updating the Topology Service Gem + +The Topology Service is developed in its [own repository](https://gitlab.com/gitlab-org/cells/topology-service) +We generate the Ruby Gem there, and manually copy the Gem to GitLab vendors folder, in +`vendor/gems/gitlab-topology-service-client`. + +To make it easy, you can just run this bash script: + +```shell +bash scripts/update-topology-service-gem.sh +``` + +This script is going to: + +1. Clone the topology service repository into a temporary folder. +1. Check if the Ruby Gem has a newer code. +1. If so, it will update the Gem in `vendor/gems/gitlab-topology-service-client` and create a commit. +1. Clean up the temporary repository. diff --git a/doc/development/documentation/help.md b/doc/development/documentation/help.md index d2e6cdaa755..13d8ced5544 100644 --- a/doc/development/documentation/help.md +++ b/doc/development/documentation/help.md @@ -80,8 +80,7 @@ The `help_page_path` contains the path to the document you want to link to, with the following conventions: - It's relative to the `doc/` directory in the GitLab repository. -- It omits the `.md` extension. -- It doesn't end with a forward slash (`/`). +- For clarity, it should end with the `.md` file extension. The help text follows the [Pajamas guidelines](https://design.gitlab.com/usability/contextual-help#formatting-help-content). @@ -94,14 +93,14 @@ is inside `_()` so it can be translated: link to the `/help` page is: ```haml - = link_to _('Learn more.'), help_page_path('user/permissions'), target: '_blank', rel: 'noopener noreferrer' + = link_to _('Learn more.'), help_page_path('user/permissions.md'), target: '_blank', rel: 'noopener noreferrer' ``` - Linking to an anchor link. Use `anchor` as part of the `help_page_path` method: ```haml - = link_to _('Learn more.'), help_page_path('user/permissions', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer' + = link_to _('Learn more.'), help_page_path('user/permissions.md', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer' ``` - Using links inline of some text. First, define the link, and then use it. In @@ -109,7 +108,7 @@ is inside `_()` so it can be translated: link: ```haml - - link = link_to('', help_page_path('user/permissions'), target: '_blank', rel: 'noopener noreferrer') + - link = link_to('', help_page_path('user/permissions.md'), target: '_blank', rel: 'noopener noreferrer') %p= safe_format(_("This is a text describing the option/feature in a sentence. %{link_start}Learn more.%{link_end}"), tag_pair(link, :link_start, :link_end)) ``` @@ -117,7 +116,7 @@ is inside `_()` so it can be translated: the rest of the page layout: ```haml - = render Pajamas::ButtonComponent.new(variant: :default, href: help_page_path('user/group/import/index'), target: '_blank') do + = render Pajamas::ButtonComponent.new(variant: :default, href: help_page_path('user/group/import/index.md'), target: '_blank') do = _('Learn more') ``` @@ -128,7 +127,7 @@ To link to the documentation from a JavaScript or a Vue component, use the `help ```javascript import { helpPagePath } from '~/helpers/help_page_helper'; -helpPagePath('user/permissions', { anchor: 'anchor-link' }) +helpPagePath('user/permissions.md', { anchor: 'anchor-link' }) // evaluates to '/help/user/permissions#anchor-link' for GitLab.com ``` @@ -140,7 +139,7 @@ To link to the documentation from within Ruby code, use the following code block be translated: ```ruby -docs_link = link_to _('Learn more.'), help_page_url('user/permissions', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer' +docs_link = link_to _('Learn more.'), help_page_url('user/permissions.md', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer' safe_format(_('This is a text describing the option/feature in a sentence. %{docs_link}'), docs_link: docs_link) ``` @@ -148,7 +147,7 @@ In cases where you need to generate a link from outside of views/helpers, where as a guide where the methods are fully qualified: ```ruby -docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/permissions', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer' +docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/permissions.md', anchor: 'anchor-link'), target: '_blank', rel: 'noopener noreferrer' safe_format(_('This is a text describing the option/feature in a sentence. %{docs_link}'), docs_link: docs_link) ``` diff --git a/doc/topics/git/undo.md b/doc/topics/git/undo.md index b8852b74ca4..82bb267dc68 100644 --- a/doc/topics/git/undo.md +++ b/doc/topics/git/undo.md @@ -361,6 +361,7 @@ git rebase -i commit-id > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/450701) in GitLab 17.1 [with a flag](../../administration/feature_flags.md) named `rewrite_history_ui`. Disabled by default. > - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/462999) in GitLab 17.2. +> - [Enabled on self-managed and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/462999) in GitLab 17.3. FLAG: The availability of this feature is controlled by a feature flag. diff --git a/doc/user/gitlab_duo/turn_on_off.md b/doc/user/gitlab_duo/turn_on_off.md index e9aa664711b..b821569eabf 100644 --- a/doc/user/gitlab_duo/turn_on_off.md +++ b/doc/user/gitlab_duo/turn_on_off.md @@ -60,6 +60,33 @@ To resolve this problem, try editing your Apache proxy settings: RewriteRule ^/?(.*) "ws://127.0.0.1:8181/$1" [P,L] ``` +## Run a health check for GitLab Duo + +DETAILS: +**Offering:** Self-managed, GitLab Dedicated +**Tier:** Premium, Ultimate +**Status:** Beta + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161997) in GitLab 17.3 [with a flag](../../administration/feature_flags.md) named `cloud_connector_status`. Enabled by default. + +FLAG: +The availability of this feature is controlled by a feature flag. +For more information, see the history. +This feature is available for testing, but not ready for production use. + +Run a health check to test if your instance meets the requirements to use GitLab Duo. +When the health check completes, it displays a pass or fail result and the types of issues. + +Prerequisites: + +- You must be an administrator. + +To run a health check: + +1. On the left sidebar, at the bottom, select **Admin**. +1. Select **GitLab Duo**. +1. On the upper-right corner, select **Run health check**. + ## Turn off GitLab Duo features You can turn off GitLab Duo for a group, project, or instance. diff --git a/doc/user/project/repository/reducing_the_repo_size_using_git.md b/doc/user/project/repository/reducing_the_repo_size_using_git.md index 7c452ec853f..5fad674b701 100644 --- a/doc/user/project/repository/reducing_the_repo_size_using_git.md +++ b/doc/user/project/repository/reducing_the_repo_size_using_git.md @@ -246,6 +246,7 @@ When using repository cleanup, note: > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/450701) in GitLab 17.1 [with a flag](../../../administration/feature_flags.md) named `rewrite_history_ui`. Disabled by default. > - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/462999) in GitLab 17.2. +> - [Enabled on self-managed and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/462999) in GitLab 17.3. FLAG: The availability of this feature is controlled by a feature flag. diff --git a/lib/gitlab/background_migration/alter_webhook_deleted_audit_event.rb b/lib/gitlab/background_migration/alter_webhook_deleted_audit_event.rb new file mode 100644 index 00000000000..b720287f40d --- /dev/null +++ b/lib/gitlab/background_migration/alter_webhook_deleted_audit_event.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + class AlterWebhookDeletedAuditEvent < Gitlab::BackgroundMigration::BatchedMigrationJob + include Gitlab::Database::DynamicModelHelpers + + feature_category :webhooks + operation_name :alter_webhook_deleted_audit_event + + def perform + each_sub_batch do |sub_batch| + sub_batch.where(target_type: %w[SystemHook GroupHook ProjectHook]) + .where("target_details NOT LIKE 'Hook%'") + .update_all("target_details = regexp_replace(target_details, '.*', 'Hook ' || target_id::text)") + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6dd260cc048..364353fc88f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -53668,6 +53668,12 @@ msgstr "" msgid "The pipeline has been deleted" msgstr "" +msgid "The policy is disabled" +msgstr "" + +msgid "The policy is enabled" +msgstr "" + msgid "The project has already been added to your dashboard." msgstr "" @@ -57214,15 +57220,27 @@ msgstr "" msgid "UsageQuotas|Container Registry storage statistics are not used to calculate the total project storage. Total project storage is calculated after namespace container deduplication, where the total of all unique containers is added to the namespace storage total." msgstr "" -msgid "UsageQuotas|Health check failed" +msgid "UsageQuotas|Failed" msgstr "" -msgid "UsageQuotas|Health check succeeded" +msgid "UsageQuotas|GitLab Duo should be operational." +msgstr "" + +msgid "UsageQuotas|Health check results" msgstr "" msgid "UsageQuotas|Namespace transfer data used" msgstr "" +msgid "UsageQuotas|Not operational. Resolve issues to use GitLab Duo." +msgstr "" + +msgid "UsageQuotas|Passed" +msgstr "" + +msgid "UsageQuotas|Tests are running..." +msgstr "" + msgid "UsageQuota|%{linkTitle} help link" msgstr "" diff --git a/scripts/update-topology-service-gem.sh b/scripts/update-topology-service-gem.sh new file mode 100755 index 00000000000..798bd5bcddd --- /dev/null +++ b/scripts/update-topology-service-gem.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -eo pipefail +## Run this script at the root of the GitLab Rails app + +repo=https://gitlab.com/gitlab-org/cells/topology-service.git +ref=${REF:-main} +tmp=tmp/gitlab-topology-service-client +gem_source="$tmp/clients/ruby" +gem_target="vendor/gems/gitlab-topology-service-client" +files_list="$gem_source/.sync" + +## Check if there are uncommitted changes +if git diff --exit-code; then + echo "Clean repo" +else + echo "There are uncommitted changes. Please commit them and then run this command" + exit 1 +fi + +# Cloning the Topology Service Repo into a temporary directory +rm -rf "$tmp" +git clone --single-branch --branch "$ref" "$repo" "$tmp" +echo "Checked out ${ref}" + +if [[ -f $files_list ]]; then + echo "List of files to sync exists. Proceeding" +else + echo "The checkout out revision doesn't contain a list of files to sync in path: clients/ruby/.sync" + exit 1 +fi + +prev_rev=$(cat "$gem_target/REVISION") +rev=$(git -C "$tmp" rev-parse HEAD) +short_rev=$(git -C "$tmp" rev-parse --short HEAD) + +## Synchronize (create/update/delete) files +rsync -arv --delete --files-from="$files_list" "$gem_source" "$gem_target" + +## Commit Changes +git add $gem_target +if git diff --exit-code HEAD $gem_target; then + echo "No changes to commit" +else + echo "Committing code" + echo "$rev" > "$gem_target/REVISION" + git add "$gem_target/REVISION" + changelog=$(git -C "$tmp" log --no-merges --pretty="- %h: %s" "$prev_rev..$rev" -- clients/ruby/) + git commit -m "Updating Topology Service Client Gem to $short_rev" -m "$changelog" -m 'changelog: other' +fi + +rm -rf "$tmp" diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb index 9ea6e8820f7..c1b6b79faad 100644 --- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb +++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb @@ -181,10 +181,22 @@ RSpec.describe Groups::DependencyProxyForContainersController, feature_category: group.add_guest(user) end - # When authenticating with a job token, the encoded token is the same as - # that built when authenticating with a user + # When authenticating with a job token, the encoded token has the same structure + # as the token built when authenticating with a user context 'with a valid user or a job token' do it_behaves_like 'sends Workhorse instructions' + + context 'with a job token triggered by a group access token user' do + let_it_be(:user) { create(:user, :project_bot) } + + it_behaves_like 'sends Workhorse instructions' + end + + context 'with a job token triggered by a service account user' do + let_it_be(:user) { create(:user, :service_account) } + + it_behaves_like 'sends Workhorse instructions' + end end context 'with a valid group access token' do diff --git a/spec/factories/import_source_users.rb b/spec/factories/import_source_users.rb index d6b5d25d73a..9dce9048fd9 100644 --- a/spec/factories/import_source_users.rb +++ b/spec/factories/import_source_users.rb @@ -26,6 +26,11 @@ FactoryBot.define do status { 1 } end + trait :reassignment_in_progress do + with_reassign_to_user + status { 2 } + end + trait :completed do status { 5 } end diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb index ccc3eb36c54..a039a626efa 100644 --- a/spec/factories/project_members.rb +++ b/spec/factories/project_members.rb @@ -26,12 +26,6 @@ FactoryBot.define do after(:build) { |project_member, _| project_member.user.block! } end - trait :banned do - after(:create) do |member| - create(:namespace_ban, namespace: member.member_namespace.root_ancestor, user: member.user) unless member.owner? - end - end - trait :awaiting do after(:create) do |member| member.update!(state: ::Member::STATE_AWAITING) diff --git a/spec/finders/import/source_users_finder_spec.rb b/spec/finders/import/source_users_finder_spec.rb index ba08c54440d..941b23fdfbd 100644 --- a/spec/finders/import/source_users_finder_spec.rb +++ b/spec/finders/import/source_users_finder_spec.rb @@ -7,7 +7,10 @@ RSpec.describe Import::SourceUsersFinder, feature_category: :importers do let_it_be(:group) { create(:group) } let_it_be(:source_user_1) { create(:import_source_user, namespace: group, status: 0, source_name: 'b') } let_it_be(:source_user_2) { create(:import_source_user, namespace: group, status: 1, source_name: 'c') } - let_it_be(:source_user_3) { create(:import_source_user, namespace: group, status: 2, source_name: 'a') } + let_it_be(:source_user_3) do + create(:import_source_user, :with_reassign_to_user, namespace: group, status: 2, source_name: 'a') + end + let_it_be(:import_source_users) { [source_user_1, source_user_2, source_user_3] } let(:params) { {} } diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js index 1ca75e5289b..b363ab48126 100644 --- a/spec/frontend/clusters_list/components/agent_table_spec.js +++ b/spec/frontend/clusters_list/components/agent_table_spec.js @@ -24,7 +24,6 @@ const defaultConfigHelpUrl = const provideData = { kasCheckVersion: '14.8.0', - projectPath: 'path/to/project', }; const defaultProps = { agents: clusterAgents, diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js index 3cfaf01d046..51d15dc18f6 100644 --- a/spec/frontend/clusters_list/components/agents_spec.js +++ b/spec/frontend/clusters_list/components/agents_spec.js @@ -11,6 +11,7 @@ import { AGENT_FEEDBACK_ISSUE, } from '~/clusters_list/constants'; import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql'; +import getTreeListQuery from '~/clusters_list/graphql/queries/get_tree_list.query.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; @@ -28,6 +29,11 @@ describe('Agents', () => { projectPath: 'path/to/project', }; + const agentProject = { + id: '1', + fullPath: 'path/to/project', + }; + const createWrapper = async ({ props = {}, glFeatures = {}, @@ -37,7 +43,6 @@ describe('Agents', () => { trees = [], queryResponse = null, }) => { - const provide = provideData; const queryResponseData = { data: { project: { @@ -53,14 +58,32 @@ describe('Agents', () => { userAccessAuthorizedAgents: { nodes: userAccessAuthorizedAgentsNodes, }, - repository: { tree: { trees: { nodes: trees } } }, }, }, }; - const agentQueryResponse = - queryResponse || jest.fn().mockResolvedValue(queryResponseData, provide); + const treeListResponseData = { + data: { + project: { + id: 'gid://gitlab/Project/1', + repository: { + tree: { + trees: { nodes: trees }, + }, + }, + }, + }, + }; + const agentQueryResponse = queryResponse || jest.fn().mockResolvedValue(queryResponseData); + const treeListQueryResponse = jest.fn().mockResolvedValue(treeListResponseData); - const apolloProvider = createMockApollo([[getAgentsQuery, agentQueryResponse]]); + const apolloProvider = createMockApollo( + [ + [getAgentsQuery, agentQueryResponse], + [getTreeListQuery, treeListQueryResponse], + ], + {}, + { typePolicies: { Query: { fields: { project: { merge: true } } } } }, + ); wrapper = shallowMount(Agents, { apolloProvider, @@ -101,6 +124,7 @@ describe('Agents', () => { userAccessAuthorizations: null, connections: null, tokens: null, + project: agentProject, }, { __typename: 'ClusterAgent', @@ -118,6 +142,7 @@ describe('Agents', () => { }, ], }, + project: agentProject, }, ]; const ciAccessAuthorizedAgentsNodes = [ @@ -131,6 +156,7 @@ describe('Agents', () => { userAccessAuthorizations: null, connections: null, tokens: null, + project: agentProject, }, }, ]; @@ -161,6 +187,7 @@ describe('Agents', () => { lastContact: null, connections: null, tokens: null, + project: agentProject, }, { id: '2', @@ -181,6 +208,7 @@ describe('Agents', () => { }, ], }, + project: agentProject, }, { id: '3', @@ -192,6 +220,7 @@ describe('Agents', () => { lastContact: null, connections: null, tokens: null, + project: agentProject, }, ]; diff --git a/spec/frontend/clusters_list/components/mock_data.js b/spec/frontend/clusters_list/components/mock_data.js index aea70e1b6f2..e2104e673aa 100644 --- a/spec/frontend/clusters_list/components/mock_data.js +++ b/spec/frontend/clusters_list/components/mock_data.js @@ -26,6 +26,9 @@ export const clusterAgents = [ webPath: '/agent-1', status: 'unused', lastContact: null, + project: { + fullPath: 'path/to/project', + }, userAccessAuthorizations: { config: { access_as: { agent: {} }, @@ -39,6 +42,9 @@ export const clusterAgents = [ webPath: '/agent-2', status: 'active', lastContact: connectedTimeNow.getTime(), + project: { + fullPath: 'path/to/project', + }, userAccessAuthorizations: null, connections: { nodes: [ @@ -65,6 +71,9 @@ export const clusterAgents = [ status: 'inactive', lastContact: connectedTimeInactive.getTime(), userAccessAuthorizations: null, + project: { + fullPath: 'path/to/project', + }, connections: { nodes: [ { @@ -86,6 +95,9 @@ export const clusterAgents = [ webPath: '/agent-4', status: 'inactive', lastContact: connectedTimeInactive.getTime(), + project: { + fullPath: 'path/to/project', + }, userAccessAuthorizations: null, connections: { nodes: [ @@ -111,6 +123,9 @@ export const clusterAgents = [ webPath: '/agent-5', status: 'inactive', lastContact: connectedTimeInactive.getTime(), + project: { + fullPath: 'path/to/project', + }, userAccessAuthorizations: null, connections: { nodes: [ @@ -136,6 +151,9 @@ export const clusterAgents = [ webPath: '/agent-6', status: 'inactive', lastContact: connectedTimeInactive.getTime(), + project: { + fullPath: 'path/to/project', + }, userAccessAuthorizations: null, connections: { nodes: [ @@ -158,6 +176,9 @@ export const clusterAgents = [ webPath: '/agent-7', status: 'inactive', lastContact: connectedTimeInactive.getTime(), + project: { + fullPath: 'path/to/project', + }, userAccessAuthorizations: null, connections: { nodes: [ @@ -180,6 +201,9 @@ export const clusterAgents = [ webPath: '/agent-8', status: 'inactive', lastContact: connectedTimeInactive.getTime(), + project: { + fullPath: 'path/to/project', + }, userAccessAuthorizations: null, connections: { nodes: [ @@ -202,6 +226,9 @@ export const clusterAgents = [ webPath: '/agent-9', status: 'inactive', lastContact: connectedTimeInactive.getTime(), + project: { + fullPath: 'path/to/project', + }, userAccessAuthorizations: null, connections: { nodes: [ @@ -224,6 +251,9 @@ export const clusterAgents = [ webPath: 'shared-project/agent-1', status: 'inactive', lastContact: connectedTimeInactive.getTime(), + project: { + fullPath: 'path/to/project', + }, userAccessAuthorizations: null, isShared: true, connections: null, diff --git a/spec/frontend/clusters_list/mocks/apollo.js b/spec/frontend/clusters_list/mocks/apollo.js index 55839ac36de..cbafba15c62 100644 --- a/spec/frontend/clusters_list/mocks/apollo.js +++ b/spec/frontend/clusters_list/mocks/apollo.js @@ -5,6 +5,10 @@ const agent = { webPath: 'agent-webPath', createdAt: new Date(), userAccessAuthorizations: null, + project: { + id: '1', + fullPath: 'path/to/project', + }, }; const token = { id: 'token-id', diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js index 611a496389a..0533c0da4ef 100644 --- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js +++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js @@ -157,7 +157,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov it('renders connect to agent modal', () => { expect(findConnectModal().props()).toEqual({ agentId: 'gid://gitlab/ClusterAgent/1', - projectPath: 'path/to/project', + projectPath: 'path/to/agent/project', isConfigured: true, }); }); diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js index 110d959988b..27c9b439a86 100644 --- a/spec/frontend/environments/graphql/mock_data.js +++ b/spec/frontend/environments/graphql/mock_data.js @@ -806,6 +806,7 @@ export const agent = { name: 'agent-name', webPath: 'path/to/agent-page', tokens: { nodes: [] }, + project: { id: '1', fullPath: 'path/to/agent/project' }, }; export const kubernetesNamespace = 'agent-namespace'; diff --git a/spec/lib/gitlab/background_migration/alter_webhook_deleted_audit_event_spec.rb b/spec/lib/gitlab/background_migration/alter_webhook_deleted_audit_event_spec.rb new file mode 100644 index 00000000000..5b87c112ad3 --- /dev/null +++ b/spec/lib/gitlab/background_migration/alter_webhook_deleted_audit_event_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::AlterWebhookDeletedAuditEvent, feature_category: :webhooks do + let(:migration) do + described_class.new( + start_id: audit_event1.id, + end_id: audit_event4.id, + batch_table: :audit_events, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: ApplicationRecord.connection + ) + end + + let(:audit_events) { table(:audit_events) } + let(:attributes) do + { author_id: 1, + entity_id: 2, + entity_type: "Group", + target_id: 5 } + end + + let!(:audit_event1) do + audit_events.create!(attributes.merge(id: 1, target_type: 'SystemHook', target_details: "Hook 1")) + end + + let!(:audit_event2) do + audit_events.create!(attributes.merge(id: 2, target_type: 'GroupHook', target_details: "http://example2@example.com")) + end + + let!(:audit_event3) do + audit_events.create!(attributes.merge(id: 3, target_type: 'ProjectHook', target_details: "http://example3@example.com")) + end + + let!(:audit_event4) do + audit_events.create!(attributes.merge(id: 4, target_type: 'User', target_details: "Administrator")) + end + + describe '#perform' do + subject(:perform_migration) { migration.perform } + + it 'alters target details column' do + perform_migration + + audit_events = AuditEvent.all.sort + + expect(audit_events[0].target_details).to eq("Hook 1") + expect(audit_events[1].target_details).to eq("Hook 5") + expect(audit_events[2].target_details).to eq("Hook 5") + expect(audit_events[3].target_details).to eq("Administrator") + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_with_recent_size_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_with_recent_size_spec.rb index 2884fb9b10b..2432ed8106c 100644 --- a/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_with_recent_size_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_with_recent_size_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::BackfillProjectStatisticsStorageSizeWithRecentSize, schema: 20230823090001, + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/477992', feature_category: :consumables_cost_management do include MigrationHelpers::ProjectStatisticsHelper diff --git a/spec/migrations/20240729133817_queue_alter_webhook_deleted_audit_event_spec.rb b/spec/migrations/20240729133817_queue_alter_webhook_deleted_audit_event_spec.rb new file mode 100644 index 00000000000..ccf39e4ed73 --- /dev/null +++ b/spec/migrations/20240729133817_queue_alter_webhook_deleted_audit_event_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueAlterWebhookDeletedAuditEvent, feature_category: :webhooks 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: :audit_events, + 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/import/source_user_placeholder_reference_spec.rb b/spec/models/import/source_user_placeholder_reference_spec.rb index e2cdaf4a3ee..f4f000eef47 100644 --- a/spec/models/import/source_user_placeholder_reference_spec.rb +++ b/spec/models/import/source_user_placeholder_reference_spec.rb @@ -37,6 +37,60 @@ RSpec.describe Import::SourceUserPlaceholderReference, feature_category: :import end end + describe 'scopes' do + describe '.model_groups_for_source_user' do + let_it_be(:source_user_1) { create(:import_source_user) } + let_it_be(:source_user_2) { create(:import_source_user) } + + let_it_be(:issue_references_1) do + create_list( + :import_source_user_placeholder_reference, + 2, + source_user: source_user_1, + model: Issue.to_s, + user_reference_column: 'author_id' + ) + end + + let_it_be(:note_author_id_reference_1) do + create( + :import_source_user_placeholder_reference, + source_user: source_user_1, + model: Note.to_s, + user_reference_column: 'author_id' + ) + end + + let_it_be(:note_updated_by_id_reference_1) do + create( + :import_source_user_placeholder_reference, + source_user: source_user_1, + model: Note.to_s, + user_reference_column: 'updated_by_id' + ) + end + + let_it_be(:merge_request_reference_2) do + create( + :import_source_user_placeholder_reference, + source_user: source_user_2, + model: MergeRequest.to_s, + user_reference_column: 'author_id' + ) + end + + it 'returns groups of models and user reference columns for a source user' do + mapped_reference_groups = described_class + .model_groups_for_source_user(source_user_1) + .map { |reference_group| [reference_group.model, reference_group.user_reference_column] } + + expect(mapped_reference_groups).to match_array( + [%w[Note author_id], %w[Note updated_by_id], %w[Issue author_id]] + ) + end + end + end + it 'is destroyed when source user is destroyed' do reference = create(:import_source_user_placeholder_reference) @@ -184,4 +238,186 @@ RSpec.describe Import::SourceUserPlaceholderReference, feature_category: :import end end end + + describe 'model_record methods' do + let_it_be(:source_user_1) { create(:import_source_user) } + let_it_be(:source_user_2) { create(:import_source_user) } + + # Issue + let_it_be(:issue_author_id_1) { create(:issue, author_id: source_user_1.placeholder_user_id) } + let_it_be(:issue_author_id_2) { create(:issue, author_id: source_user_2.placeholder_user_id) } + let_it_be(:issue_closed_by_id_1) { create(:issue, closed_by_id: source_user_1.placeholder_user_id) } + let_it_be(:issue_author_id_reference_1) do + create( + :import_source_user_placeholder_reference, + source_user: source_user_1, + model: Issue.to_s, + user_reference_column: 'author_id', + numeric_key: issue_author_id_1.id + ) + end + + let_it_be(:issue_author_id_reference_2) do + create( + :import_source_user_placeholder_reference, + source_user: source_user_2, + model: Issue.to_s, + user_reference_column: 'author_id', + numeric_key: issue_author_id_2.id + ) + end + + let_it_be(:issue_closed_by_id_reference_1) do + create( + :import_source_user_placeholder_reference, + source_user: source_user_1, + model: Issue.to_s, + user_reference_column: 'closed_by_id', + numeric_key: issue_closed_by_id_1.id + ) + end + + # IssueAssignee + let_it_be(:issue_assignee_1) do + issue_author_id_1.issue_assignees.create!( + user_id: source_user_1.placeholder_user_id, issue_id: issue_author_id_1.id + ) + end + + let_it_be(:issue_assignee_2) do + issue_author_id_1.issue_assignees.create!( + user_id: source_user_2.placeholder_user_id, issue_id: issue_author_id_1.id + ) + end + + let_it_be(:issue_assignee_reference_1) do + create( + :import_source_user_placeholder_reference, + source_user: source_user_1, + model: IssueAssignee.to_s, + user_reference_column: 'user_id', + numeric_key: nil, + composite_key: { user_id: source_user_1.placeholder_user_id, issue_id: issue_author_id_1.id } + ) + end + + let_it_be(:issue_assignee_reference_2) do + create( + :import_source_user_placeholder_reference, + source_user: source_user_2, + model: IssueAssignee.to_s, + user_reference_column: 'user_id', + numeric_key: nil, + composite_key: { user_id: source_user_2.placeholder_user_id, issue_id: issue_author_id_1.id } + ) + end + + describe '#model_record' do + it 'returns the numeric pkey model record the placeholder reference refers to' do + expect(issue_author_id_reference_1.model_record).to eq(issue_author_id_1) + end + + it 'returns the composite key model record the placeholder reference refers to' do + record = issue_assignee_reference_1.model_record + + expect([record.user_id, record.issue_id]).to eq([issue_assignee_1.user_id, issue_assignee_1.issue_id]) + end + + context 'when the model record no longer belongs the reference\'s placeholder user' do + let!(:another_user) { create(:user) } + + before do + issue_closed_by_id_1.update!(closed_by_id: another_user.id) + end + + it 'does not return the record' do + expect(issue_closed_by_id_reference_1.model_record).to be_nil + end + end + + context 'when the placeholder reference does not map to a real model' do + let!(:invalid_model) { 'ThisWillNeverMapToARealModel' } + let!(:user_reference_column) { 'user_id' } + + let!(:invalid_placeholder_reference) do + create( + :import_source_user_placeholder_reference, + source_user: source_user_1, + model: invalid_model, + user_reference_column: user_reference_column + ) + end + + it 'raises an error' do + expect { invalid_placeholder_reference.model_record }.to raise_error(NameError) + end + end + end + + describe '.model_relations_for_source_user_reference', :aggregate_failures do + it 'yields numeric pkey model relations and placeholder reference relation' do + expect do |block| + described_class.model_relations_for_source_user_reference( + model: 'Issue', source_user: source_user_1, user_reference_column: 'author_id', &block + ) + end.to yield_with_args( + [ + match_array(issue_author_id_1), + match_array(issue_author_id_reference_1) + ] + ) + end + + it 'yields composite key model relation and placeholder reference relation' do + expect do |block| + described_class.model_relations_for_source_user_reference( + model: 'IssueAssignee', source_user: source_user_1, user_reference_column: 'user_id', &block + ) + end.to yield_with_args( + [ + match_array(have_attributes(user_id: issue_assignee_1.user_id, issue_id: issue_assignee_1.issue_id)), + match_array(issue_assignee_reference_1) + ] + ) + end + + context 'when a placeholder record exists but the record does not belong to a placeholder' do + let!(:another_user) { create(:user) } + + before do + issue_closed_by_id_1.update!(closed_by_id: another_user.id) + end + + it 'does not yield the record' do + expect do |block| + described_class.model_relations_for_source_user_reference( + model: 'Issue', source_user: source_user_1, user_reference_column: 'closed_by_id', &block + ) + end.not_to yield_control + end + end + + context 'when a placeholder reference does not map to a real model' do + let!(:invalid_model) { 'ThisWillNeverMapToARealModel' } + let!(:user_reference_column) { 'user_id' } + + let!(:invalid_placeholder_reference) do + create( + :import_source_user_placeholder_reference, + source_user: source_user_1, + model: invalid_model, + user_reference_column: user_reference_column + ) + end + + it 'raises an error' do + expect do |block| + described_class.model_relations_for_source_user_reference( + model: invalid_model, source_user: source_user_1, user_reference_column: user_reference_column, &block + ) + end.to raise_error(NameError) + end + end + end + end end diff --git a/spec/models/import/source_user_spec.rb b/spec/models/import/source_user_spec.rb index fd03b44d78b..fb97ed1ccb3 100644 --- a/spec/models/import/source_user_spec.rb +++ b/spec/models/import/source_user_spec.rb @@ -19,6 +19,13 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do it { is_expected.not_to validate_presence_of(:placeholder_user_id) } end + + it 'validates reassign_to_user_id if status is reassignment_in_progress' do + import_source_user = build(:import_source_user, :reassignment_in_progress, reassign_to_user: nil) + + expect(import_source_user).to be_invalid + expect(import_source_user.errors[:reassign_to_user_id]).to eq(["can't be blank"]) + end end describe 'scopes' do @@ -62,9 +69,15 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do it 'begins in pending state' do expect(described_class.new.pending_reassignment?).to eq(true) end + + it 'does not transition to reassignment_in_progress without a reassign_to_user' do + import_source_user = create(:import_source_user, :awaiting_approval, reassign_to_user: nil) + + expect { import_source_user.accept! }.to raise_error(StateMachines::InvalidTransition) + end end - describe 'after_transition callback' do + describe 'after_transition callbacks' do subject(:source_user) { create(:import_source_user, :awaiting_approval, :with_reassign_to_user) } it 'does not unset reassign_to_user on other transitions' do @@ -163,9 +176,11 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do describe '.sort_by_attribute' do let_it_be(:namespace) { create(:namespace) } let_it_be(:source_user_1) { create(:import_source_user, namespace: namespace, status: 4, source_name: 'd') } - let_it_be(:source_user_2) { create(:import_source_user, namespace: namespace, status: 2, source_name: 'b') } + let_it_be(:source_user_2) { create(:import_source_user, namespace: namespace, status: 3, source_name: 'c') } let_it_be(:source_user_3) { create(:import_source_user, namespace: namespace, status: 1, source_name: 'a') } - let_it_be(:source_user_4) { create(:import_source_user, namespace: namespace, status: 3, source_name: 'c') } + let_it_be(:source_user_4) do + create(:import_source_user, :with_reassign_to_user, namespace: namespace, status: 2, source_name: 'b') + end let(:sort_by_attribute) { described_class.sort_by_attribute(method).pluck(attribute) } diff --git a/spec/models/packages/composer/package_spec.rb b/spec/models/packages/composer/package_spec.rb index 2f15dd2f5cd..18ba1d3b17c 100644 --- a/spec/models/packages/composer/package_spec.rb +++ b/spec/models/packages/composer/package_spec.rb @@ -56,4 +56,8 @@ RSpec.describe Packages::Composer::Package, type: :model, feature_category: :pac expect(result).not_to include(package3) end end + + describe '.installable' do + it_behaves_like 'installable packages', :composer_package + end end diff --git a/spec/models/packages/conan/package_spec.rb b/spec/models/packages/conan/package_spec.rb index cbf2aa0af81..68aec8901a8 100644 --- a/spec/models/packages/conan/package_spec.rb +++ b/spec/models/packages/conan/package_spec.rb @@ -99,5 +99,9 @@ RSpec.describe Packages::Conan::Package, type: :model, feature_category: :packag expect(packages.first.association(:conan_metadatum)).to be_loaded end end + + describe '.installable' do + it_behaves_like 'installable packages', :conan_package + end end end diff --git a/spec/models/packages/debian/package_spec.rb b/spec/models/packages/debian/package_spec.rb index b2282132dd7..489a4adb85a 100644 --- a/spec/models/packages/debian/package_spec.rb +++ b/spec/models/packages/debian/package_spec.rb @@ -153,6 +153,10 @@ RSpec.describe Packages::Debian::Package, type: :model, feature_category: :packa it { is_expected.to contain_exactly(package1) } end + describe '.installable' do + it_behaves_like 'installable packages', :debian_package + end + describe '#incoming?' do let(:package) { build(:debian_package) } diff --git a/spec/models/packages/go/package_spec.rb b/spec/models/packages/go/package_spec.rb index 198033bf22d..5739d39585f 100644 --- a/spec/models/packages/go/package_spec.rb +++ b/spec/models/packages/go/package_spec.rb @@ -27,4 +27,8 @@ RSpec.describe Packages::Go::Package, type: :model, feature_category: :package_r end end end + + describe '.installable' do + it_behaves_like 'installable packages', :golang_package + end end diff --git a/spec/models/packages/ml_model/package_spec.rb b/spec/models/packages/ml_model/package_spec.rb index 398f8b41ea6..09315ff82a6 100644 --- a/spec/models/packages/ml_model/package_spec.rb +++ b/spec/models/packages/ml_model/package_spec.rb @@ -78,4 +78,8 @@ RSpec.describe Packages::MlModel::Package, feature_category: :mlops do end end end + + describe '.installable' do + it_behaves_like 'installable packages', :ml_model_package + end end diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb index 443c7415210..431e98d4c51 100644 --- a/spec/models/packages/package_file_spec.rb +++ b/spec/models/packages/package_file_spec.rb @@ -422,6 +422,10 @@ RSpec.describe Packages::PackageFile, type: :model, feature_category: :package_r end end + describe '.installable_statuses' do + it_behaves_like 'installable statuses' + end + describe '#file_name_for_download' do subject { package_file.file_name_for_download } diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index b9de14e68d0..dbe8bd63cb6 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -762,17 +762,7 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis end describe '.installable' do - subject { described_class.installable } - - it 'does not include non-installable packages', :aggregate_failures do - is_expected.not_to include(error_package) - is_expected.not_to include(processing_package) - end - - it 'includes installable packages', :aggregate_failures do - is_expected.to include(default_package) - is_expected.to include(hidden_package) - end + it_behaves_like 'installable packages', :maven_package end describe '.with_status' do @@ -854,6 +844,10 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis end end + describe '.installable_statuses' do + it_behaves_like 'installable statuses' + end + describe '#versions' do let_it_be(:project) { create(:project) } let_it_be(:package) { create(:maven_package, project: project) } diff --git a/spec/models/packages/rpm/package_spec.rb b/spec/models/packages/rpm/package_spec.rb index 24dcd712ff1..04290adda87 100644 --- a/spec/models/packages/rpm/package_spec.rb +++ b/spec/models/packages/rpm/package_spec.rb @@ -6,4 +6,8 @@ RSpec.describe Packages::Rpm::Package, type: :model, feature_category: :package_ describe 'associations' do it { is_expected.to have_one(:rpm_metadatum).inverse_of(:package).class_name('Packages::Rpm::Metadatum') } end + + describe '.installable' do + it_behaves_like 'installable packages', :rpm_package + end end diff --git a/spec/models/packages/rpm/repository_file_spec.rb b/spec/models/packages/rpm/repository_file_spec.rb index b161ebe0dbb..632718bc333 100644 --- a/spec/models/packages/rpm/repository_file_spec.rb +++ b/spec/models/packages/rpm/repository_file_spec.rb @@ -6,6 +6,10 @@ RSpec.describe Packages::Rpm::RepositoryFile, type: :model, feature_category: :p let_it_be(:repository_file) { create(:rpm_repository_file) } + let_it_be(:pending_destruction_repository_package_file) do + create(:rpm_repository_file, :pending_destruction) + end + it_behaves_like 'having unique enum values' describe 'relationships' do @@ -57,14 +61,23 @@ RSpec.describe Packages::Rpm::RepositoryFile, type: :model, feature_category: :p end context 'with status scopes' do - let_it_be(:pending_destruction_repository_package_file) do - create(:rpm_repository_file, :pending_destruction) - end - describe '.with_status' do subject { described_class.with_status(:pending_destruction) } it { is_expected.to contain_exactly(pending_destruction_repository_package_file) } end end + + describe '.installable' do + subject { described_class.installable } + + it 'does not include non-displayable rpm repository files', :aggregate_failures do + is_expected.to include(repository_file) + is_expected.not_to include(pending_destruction_repository_package_file) + end + end + + describe '.installable_statuses' do + it_behaves_like 'installable statuses' + end end diff --git a/spec/models/packages/rubygems/package_spec.rb b/spec/models/packages/rubygems/package_spec.rb index 91aaf191b5e..972566d0ec9 100644 --- a/spec/models/packages/rubygems/package_spec.rb +++ b/spec/models/packages/rubygems/package_spec.rb @@ -8,4 +8,8 @@ RSpec.describe Packages::Rubygems::Package, type: :model, feature_category: :pac describe 'associations' do it { is_expected.to have_one(:rubygems_metadatum).inverse_of(:package).class_name('Packages::Rubygems::Metadatum') } end + + describe '.installable' do + it_behaves_like 'installable packages', :rubygems_package + end end diff --git a/spec/requests/import/source_users_controller_spec.rb b/spec/requests/import/source_users_controller_spec.rb index 96fdc4b5c0d..9c20a809efb 100644 --- a/spec/requests/import/source_users_controller_spec.rb +++ b/spec/requests/import/source_users_controller_spec.rb @@ -52,6 +52,12 @@ RSpec.describe Import::SourceUsersController, feature_category: :importers do it { expect { accept_invite }.to change { source_user.reload.reassignment_in_progress? }.from(false).to(true) } + it 'enqueues the job to reassign contributions' do + expect(Import::ReassignPlaceholderUserRecordsWorker).to receive(:perform_async).with(source_user.id) + + accept_invite + end + it 'redirects with a notice when accepted' do accept_invite diff --git a/spec/services/import/reassign_placeholder_user_records_service_spec.rb b/spec/services/import/reassign_placeholder_user_records_service_spec.rb new file mode 100644 index 00000000000..7e43dc229b7 --- /dev/null +++ b/spec/services/import/reassign_placeholder_user_records_service_spec.rb @@ -0,0 +1,272 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::ReassignPlaceholderUserRecordsService, feature_category: :importers do + let_it_be(:namespace) { create(:namespace) } + let_it_be_with_reload(:source_user) do + create(:import_source_user, + :with_reassign_to_user, + :reassignment_in_progress, + namespace: namespace + ) + end + + let_it_be_with_reload(:other_source_user) do + create(:import_source_user, + :with_reassign_to_user, + :reassignment_in_progress, + namespace: namespace + ) + end + + let_it_be(:placeholder_user_id) { source_user.placeholder_user_id } + let_it_be(:other_placeholder_user_id) { other_source_user.placeholder_user_id } + let_it_be(:real_user_id) { source_user.reassign_to_user_id } + + # MergeRequests + let_it_be_with_reload(:merge_requests) { create_list(:merge_request, 3, author_id: placeholder_user_id) } + + let_it_be_with_reload(:other_merge_request) do + create(:merge_request, author_id: other_placeholder_user_id) + end + + # Approvals + let_it_be_with_reload(:merge_request_approval) do + create(:approval, merge_request: other_merge_request, user_id: placeholder_user_id) + end + + let_it_be_with_reload(:merge_request_approval_2) do + create(:approval, merge_request: merge_requests[0], user_id: placeholder_user_id) + end + + let_it_be_with_reload(:other_merge_request_approval) do + create(:approval, merge_request: merge_requests[0], user_id: other_placeholder_user_id) + end + + # Issues + let_it_be_with_reload(:issue) { create(:issue, author_id: placeholder_user_id, closed_by_id: placeholder_user_id) } + let_it_be_with_reload(:issue_closed) { create(:issue, closed_by_id: placeholder_user_id) } + + # IssueAssignees + let_it_be_with_reload(:issue_assignee) do + issue.issue_assignees.create!(user_id: placeholder_user_id, issue_id: issue.id) + end + + # Notes + let_it_be_with_reload(:authored_note) { create(:note, author_id: placeholder_user_id) } + let_it_be_with_reload(:updated_note) { create(:note, updated_by_id: placeholder_user_id) } + + # GroupMembers + let_it_be_with_reload(:group_member) { create(:group_member, user_id: placeholder_user_id) } + + # Ci::Builds - schema is gitlab_ci + let_it_be_with_reload(:ci_build) { create(:ci_build, user_id: placeholder_user_id) } + + subject(:service) { described_class.new(source_user) } + + before do + # Create import_source_user_placeholder_reference for memoized records + # MergeRequests + merge_requests.each { |mr| create_placeholder_reference(source_user, mr, user_column: 'author_id') } + create_placeholder_reference(other_source_user, other_merge_request, user_column: 'author_id') + + # Approvals + create_placeholder_reference(source_user, merge_request_approval, user_column: 'user_id') + create_placeholder_reference(source_user, merge_request_approval_2, user_column: 'user_id') + create_placeholder_reference(other_source_user, other_merge_request_approval, user_column: 'user_id') + + # Issues + create_placeholder_reference(source_user, issue, user_column: 'author_id') + create_placeholder_reference(source_user, issue, user_column: 'closed_by_id') + create_placeholder_reference(source_user, issue_closed, user_column: 'closed_by_id') + + # IssueAssignees + create_placeholder_reference( + source_user, + issue.issue_assignees.find_by(user_id: placeholder_user_id), + user_column: 'user_id', + composite_key: { user_id: placeholder_user_id, issue_id: issue.id } + ) + + # Notes + create_placeholder_reference(source_user, authored_note, user_column: 'author_id') + create_placeholder_reference(source_user, updated_note, user_column: 'updated_by_id') + + # GroupMembers + create_placeholder_reference(source_user, group_member, user_column: 'user_id') + + # Ci::Builds + create_placeholder_reference(source_user, ci_build, user_column: 'user_id') + end + + describe '#execute', :aggregate_failures do + shared_examples 'a successful reassignment' do + it 'completes the reassignment' do + service.execute + + expect(source_user.reload).to be_completed + end + + it 'does not update any records that do not belong to the source user' do + expect { service.execute }.to not_change { other_merge_request.reload.author_id } + .from(other_placeholder_user_id) + .and not_change { other_merge_request_approval.reload.user_id }.from(other_placeholder_user_id) + end + + it 'does not delete any placeholder references that do not belong to the source user' do + expect { service.execute }.to not_change { + Import::SourceUserPlaceholderReference.where(source_user: other_source_user).count + } + end + end + + context 'when a user can be reassigned without error' do + it 'updates actual records from the source user\'s placeholder reference records' do + expect { service.execute }.to change { merge_requests[0].reload.author_id } + .from(placeholder_user_id).to(real_user_id) + .and change { merge_requests[1].reload.author_id }.from(placeholder_user_id).to(real_user_id) + .and change { merge_requests[2].reload.author_id }.from(placeholder_user_id).to(real_user_id) + .and change { merge_request_approval.reload.user_id }.from(placeholder_user_id).to(real_user_id) + .and change { merge_request_approval_2.reload.user_id }.from(placeholder_user_id).to(real_user_id) + .and change { issue.reload.author_id }.from(placeholder_user_id).to(real_user_id) + .and change { issue.reload.closed_by_id }.from(placeholder_user_id).to(real_user_id) + .and change { issue_closed.reload.closed_by_id }.from(placeholder_user_id).to(real_user_id) + .and change { authored_note.reload.author_id }.from(placeholder_user_id).to(real_user_id) + .and change { updated_note.reload.updated_by_id }.from(placeholder_user_id).to(real_user_id) + .and change { group_member.reload.user_id }.from(placeholder_user_id).to(real_user_id) + .and change { IssueAssignee.where({ user_id: real_user_id, issue_id: issue.id }).count }.from(0).to(1) + end + + it 'deletes reassigned placeholder references for the source user' do + expect { service.execute }.to change { + Import::SourceUserPlaceholderReference.where(source_user: source_user).count + }.to(0) + end + + it_behaves_like 'a successful reassignment' + end + + context 'when the source user is not in reassignment_in_progress status' do + before do + source_user.update!(status: 0) + end + + it 'does not reassign any contributions' do + expect { service.execute }.to not_change { merge_requests[0].reload.author_id }.from(placeholder_user_id) + .and not_change { merge_requests[1].reload.author_id }.from(placeholder_user_id) + .and not_change { merge_requests[2].reload.author_id }.from(placeholder_user_id) + .and not_change { merge_request_approval.reload.user_id }.from(placeholder_user_id) + .and not_change { merge_request_approval_2.reload.user_id }.from(placeholder_user_id) + .and not_change { issue.reload.author_id }.from(placeholder_user_id) + .and not_change { authored_note.reload.author_id }.from(placeholder_user_id) + .and not_change { updated_note.reload.updated_by_id }.from(placeholder_user_id) + .and not_change { group_member.reload.user_id }.from(placeholder_user_id) + .and not_change { IssueAssignee.where({ user_id: real_user_id, issue_id: issue.id }).count }.from(0) + end + + it 'does not complete the source user' do + expect { service.execute }.to not_change { source_user.status } + end + + it 'does not delete and placeholder references' do + expect { service.execute }.to not_change { + Import::SourceUserPlaceholderReference.where(source_user: source_user).count + } + end + end + + context 'when a placeholder reference is for a nonexistant model' do + let_it_be(:invalid_model) { 'ThisWillNeverMapToARealModel' } + let_it_be(:user_reference_column) { 'user_id' } + + let_it_be(:invalid_placeholder_reference) do + create( + :import_source_user_placeholder_reference, + source_user: source_user, + model: invalid_model, + user_reference_column: user_reference_column + ) + end + + it 'logs an error' do + expect(::Import::Framework::Logger).to receive(:error).with( + hash_including( + message: "#{invalid_model} is not a model, #{user_reference_column} cannot be reassigned.", + source_user_id: source_user.id + ) + ) + + service.execute + end + + it 'does not delete the invalid placeholder reference' do + expect { service.execute }.not_to change { invalid_placeholder_reference.reload.present? }.from(true) + end + + it 'completes the reassignment' do + service.execute + + expect(source_user.reload).to be_completed + end + end + + context 'when a record is no longer unique before reassignment' do + let_it_be_with_reload(:duplicate_merge_request_approval) do + create(:approval, merge_request: other_merge_request, user_id: real_user_id) + end + + it 'updates actual records except non-uniqie record', :aggregate_failures do + expect { service.execute }.to change { merge_requests[0].reload.author_id } + .from(placeholder_user_id).to(real_user_id) + .and change { merge_requests[1].reload.author_id }.from(placeholder_user_id).to(real_user_id) + .and change { merge_requests[2].reload.author_id }.from(placeholder_user_id).to(real_user_id) + .and change { merge_request_approval_2.reload.user_id }.from(placeholder_user_id).to(real_user_id) + .and change { issue.reload.author_id }.from(placeholder_user_id).to(real_user_id) + .and change { issue.reload.closed_by_id }.from(placeholder_user_id).to(real_user_id) + .and change { issue_closed.reload.closed_by_id }.from(placeholder_user_id).to(real_user_id) + .and change { authored_note.reload.author_id }.from(placeholder_user_id).to(real_user_id) + .and change { updated_note.reload.updated_by_id }.from(placeholder_user_id).to(real_user_id) + .and change { group_member.reload.user_id }.from(placeholder_user_id).to(real_user_id) + .and change { IssueAssignee.where({ user_id: real_user_id, issue_id: issue.id }).count }.from(0).to(1) + + expect { service.execute }.not_to change { merge_request_approval.reload.user_id }.from(placeholder_user_id) + end + + it 'logs a warning' do + expect(::Import::Framework::Logger).to receive(:warn).with({ + message: "Unable to reassign record, reassigned user is invalid or not unique", + source_user_id: source_user.id + }) + + service.execute + end + + it 'does not delete placeholder references for unassigned records' do + expect { service.execute }.to change { + Import::SourceUserPlaceholderReference.where(source_user: source_user).count + }.to(1) + + expect( + Import::SourceUserPlaceholderReference.where(source_user: source_user).pluck(:numeric_key) + ).to eq([merge_request_approval.id]) + end + + it_behaves_like 'a successful reassignment' + end + end + + def create_placeholder_reference(source_user, object, user_column:, composite_key: nil) + numeric_key = object.id if composite_key.nil? + + create( + :import_source_user_placeholder_reference, + source_user: source_user, + namespace: source_user.namespace, + model: object.class.name, + user_reference_column: user_column, + numeric_key: numeric_key, + composite_key: composite_key + ) + end +end diff --git a/spec/services/import/source_users/accept_reassignment_service_spec.rb b/spec/services/import/source_users/accept_reassignment_service_spec.rb new file mode 100644 index 00000000000..a125680ffd6 --- /dev/null +++ b/spec/services/import/source_users/accept_reassignment_service_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::SourceUsers::AcceptReassignmentService, feature_category: :importers do + let(:import_source_user) { create(:import_source_user, :awaiting_approval, :with_reassign_to_user) } + let(:current_user) { import_source_user.reassign_to_user } + let(:service) { described_class.new(import_source_user, current_user: current_user) } + + describe '#execute' do + it 'returns success' do + expect(service.execute).to be_success + end + + it 'sets the source user to accepted' do + service.execute + + expect(import_source_user.reload).to be_reassignment_in_progress + end + + it 'enqueues the job to reassign contributions' do + expect(Import::ReassignPlaceholderUserRecordsWorker).to receive(:perform_async).with(import_source_user.id) + + service.execute + end + + shared_examples 'current user does not have permission to accept reassignment' do + it 'returns error no permissions' do + result = service.execute + + expect(result).to be_error + expect(result.message).to eq('You have insufficient permissions to update the import source user') + end + + it 'does not enqueue the job to reassign contributions' do + expect(Import::ReassignPlaceholderUserRecordsWorker).not_to receive(:perform_async) + + service.execute + end + end + + context 'when the current user is not the user to reassign contributions to' do + let(:current_user) { create(:user) } + + it_behaves_like 'current user does not have permission to accept reassignment' + end + + context 'when there is no user to reassign to' do + before do + import_source_user.update!(reassign_to_user: nil) + end + + it_behaves_like 'current user does not have permission to accept reassignment' + + context 'and no current user is provided' do + let(:current_user) { nil } + + it_behaves_like 'current user does not have permission to accept reassignment' + end + end + end +end diff --git a/spec/services/import/source_users/resend_notification_service_spec.rb b/spec/services/import/source_users/resend_notification_service_spec.rb index 0144a1100e0..6a95e6a1774 100644 --- a/spec/services/import/source_users/resend_notification_service_spec.rb +++ b/spec/services/import/source_users/resend_notification_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Import::SourceUsers::ResendNotificationService, feature_category: :importers do - let_it_be_with_reload(:import_source_user) { create(:import_source_user, :awaiting_approval) } + let_it_be_with_reload(:import_source_user) { create(:import_source_user, :with_reassign_to_user, :awaiting_approval) } let_it_be(:current_user) { import_source_user.namespace.owner } let(:service) { described_class.new(import_source_user, current_user: current_user) } diff --git a/spec/support/shared_examples/lint_factories_shared_examples.rb b/spec/support/shared_examples/lint_factories_shared_examples.rb index b356701fed8..5ed1d8fbf3f 100644 --- a/spec/support/shared_examples/lint_factories_shared_examples.rb +++ b/spec/support/shared_examples/lint_factories_shared_examples.rb @@ -34,7 +34,12 @@ module Support return if without_factory_defaults.empty? && with_factory_defaults.empty? - RSpec.describe "Lint factories for #{described_class}", feature_category: :shared do + # Pass model spec location as a caller to top-level example group. + # This enables the use of the correct model spec location as opposed to + # this very shared examples file path when specs are retry. + model_location = example_group.metadata.values_at(:absolute_file_path, :line_number).join(':') + + RSpec.describe "Lint factories for #{described_class}", feature_category: :shared, caller: [model_location] do include_examples 'Lint factories', with_factory_defaults, without_factory_defaults end end @@ -76,7 +81,6 @@ module Support [:ci_job_artifact, :gzip], [:ci_job_artifact, :correct_checksum], [:dependency_proxy_blob, :remote_store], - [:discussion_note_on_personal_snippet, Any], [:environment, :non_playable], [:issue_customer_relations_contact, :for_contact], [:issue_customer_relations_contact, :for_issue], diff --git a/spec/support/shared_examples/packages/installable_shared_examples.rb b/spec/support/shared_examples/packages/installable_shared_examples.rb new file mode 100644 index 00000000000..9a4e52ff6f7 --- /dev/null +++ b/spec/support/shared_examples/packages/installable_shared_examples.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'installable packages' do |factory_name| + context "for #{factory_name}", :aggregate_failures do + let_it_be(:default_package) { create(factory_name, :default) } + let_it_be(:hidden_package) { create(factory_name, :hidden) } + let_it_be(:processing_package) { create(factory_name, :processing) } + let_it_be(:error_package) { create(factory_name, :error) } + + subject { described_class.installable } + + it 'does not include non-installable packages' do + is_expected.not_to include(error_package) + is_expected.not_to include(processing_package) + end + + it 'includes installable packages' do + is_expected.to include(default_package) + is_expected.to include(hidden_package) + end + end +end + +RSpec.shared_examples 'installable statuses' do + it 'returns installable statuses' do + expect(described_class.installable_statuses).to eq(described_class::INSTALLABLE_STATUSES) + end +end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 3d0f6551b30..20b71c2c430 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -467,6 +467,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'UpdateMergeRequestsWorker' => 3, 'UpdateProjectStatisticsWorker' => 3, 'UploadChecksumWorker' => 3, + 'Import::ReassignPlaceholderUserRecordsWorker' => 5, 'Vulnerabilities::Statistics::AdjustmentWorker' => 3, 'VulnerabilityExports::ExportDeletionWorker' => 3, 'VulnerabilityExports::ExportWorker' => 3, diff --git a/spec/workers/import/reassign_placeholder_user_records_worker_spec.rb b/spec/workers/import/reassign_placeholder_user_records_worker_spec.rb new file mode 100644 index 00000000000..baa99decaf4 --- /dev/null +++ b/spec/workers/import/reassign_placeholder_user_records_worker_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::ReassignPlaceholderUserRecordsWorker, feature_category: :importers do + let(:import_source_user) do + create(:import_source_user, :with_reassign_to_user, :reassignment_in_progress) + end + + let(:job_args) { import_source_user.id } + + describe '#perform' do + before do + allow(Import::ReassignPlaceholderUserRecordsService).to receive(:new).and_call_original + end + + it_behaves_like 'an idempotent worker' do + it 'enqueues service to map records to real users' do + expect(Import::ReassignPlaceholderUserRecordsService).to receive(:new).once + + perform_multiple(job_args) + end + + shared_examples 'an invalid source user' do + it 'does not enqueue service to map records to real users' do + expect(Import::ReassignPlaceholderUserRecordsService).not_to receive(:new) + + perform_multiple(job_args) + end + + it 'logs a warning that the reassignment process was not started' do + expect(::Import::Framework::Logger).to receive(:warn).with({ + message: 'Unable to begin reassignment because Import source user has an invalid status or does not exist', + source_user_id: import_source_user&.id + }).twice + + perform_multiple(job_args) + end + end + + context 'when import source user is not reassignment_in_progress status' do + let(:import_source_user) { create(:import_source_user, :awaiting_approval) } + + it_behaves_like 'an invalid source user' + end + + context 'when import source user does not exist' do + let(:import_source_user) { nil } + let(:job_args) { [-1] } + + it_behaves_like 'an invalid source user' + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(importer_user_mapping: false) + end + + it 'does not enqueue service to map records to real users' do + expect(Import::ReassignPlaceholderUserRecordsService).not_to receive(:new) + + perform_multiple(job_args) + end + end + end + end + + describe '#sidekiq_retries_exhausted' do + it 'logs the failure and sets the source user status to failed', :aggregate_failures do + exception = StandardError.new('Some error') + + expect(::Import::Framework::Logger).to receive(:error).with({ + message: 'Failed to reassign placeholder user', + error: exception.message, + source_user_id: import_source_user.id + }) + + described_class.sidekiq_retries_exhausted_block.call({ 'args' => job_args }, exception) + + expect(import_source_user.reload).to be_failed + end + end +end diff --git a/vendor/gems/gitlab-topology-service-client/GITLAB.md b/vendor/gems/gitlab-topology-service-client/GITLAB.md index cb6ea530ae2..6b1adf131e3 100644 --- a/vendor/gems/gitlab-topology-service-client/GITLAB.md +++ b/vendor/gems/gitlab-topology-service-client/GITLAB.md @@ -5,4 +5,8 @@ This Gem is automatically generated via GRPC from the repository https://gitlab.com/gitlab-org/cells/topology-service For more information about the Topology Service: -https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/topology_service/ \ No newline at end of file +https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/topology_service/ + +## Update the Ruby Gem + +Please refer to [Cells - Topology Service Development](/doc/development/cells/topology_service.md) diff --git a/vendor/gems/gitlab-topology-service-client/REVISION b/vendor/gems/gitlab-topology-service-client/REVISION new file mode 100644 index 00000000000..258a28d2789 --- /dev/null +++ b/vendor/gems/gitlab-topology-service-client/REVISION @@ -0,0 +1 @@ +722480422308f4bdba9e5ab874b6d4752667e51a diff --git a/vendor/gems/gitlab-topology-service-client/gitlab-topology-service-client.gemspec b/vendor/gems/gitlab-topology-service-client/gitlab-topology-service-client.gemspec index b9a612f6a1f..194e5bbc243 100644 --- a/vendor/gems/gitlab-topology-service-client/gitlab-topology-service-client.gemspec +++ b/vendor/gems/gitlab-topology-service-client/gitlab-topology-service-client.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |spec| spec.license = "MIT" spec.required_ruby_version = ">= 2.6.0" - spec.files = Dir['lib/**/*.rb'] + spec.files = Dir["lib/**/*.rb"] spec.require_paths = ["lib"] spec.add_dependency "grpc"