Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-08-08 15:11:00 +00:00
parent f9c5d4877e
commit eaaf34e042
76 changed files with 1456 additions and 87 deletions

View File

@ -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:

View File

@ -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"

View File

@ -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'

View File

@ -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 {
<connect-to-agent-modal
v-if="selectedAgent"
:agent-id="selectedAgent.id"
:project-path="projectPath"
:project-path="selectedAgent.project.fullPath"
:is-configured="isUserAccessConfigured"
/>
</div>

View File

@ -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,

View File

@ -59,7 +59,6 @@ export default {
},
getAgentsQueryVariables() {
return {
defaultBranchName: this.defaultBranchName,
projectPath: this.projectPath,
};
},

View File

@ -3,6 +3,10 @@ fragment ClusterAgentFragment on ClusterAgent {
name
webPath
createdAt
project {
id
fullPath
}
userAccessAuthorizations {
config
}

View File

@ -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
}
}
}
}
}
}

View File

@ -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
}
}
}
}
}
}

View File

@ -57,7 +57,7 @@ export default {
directives: {
GlModalDirective,
},
inject: ['kasTunnelUrl', 'projectPath'],
inject: ['kasTunnelUrl'],
props: {
environmentName: {
type: String,
@ -268,7 +268,7 @@ export default {
<connect-to-agent-modal
:agent-id="clusterAgent.id"
:project-path="projectPath"
:project-path="clusterAgent.project.fullPath"
:is-configured="true"
/>
</div>

View File

@ -10,6 +10,10 @@ query getEnvironmentClusterAgent($projectFullPath: ID!, $environmentName: String
id
name
webPath
project {
id
fullPath
}
tokens {
nodes {
id

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -252,6 +252,10 @@ class Packages::Package < ApplicationRecord
end
end
def self.installable_statuses
INSTALLABLE_STATUSES
end
def versions
project.packages
.preload_pipelines

View File

@ -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

View File

@ -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

View File

@ -30,6 +30,10 @@ module Packages
size: [FILELISTS_SIZE_LIMITATION..]
).exists?
end
def self.installable_statuses
INSTALLABLE_STATUSES
end
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -407,6 +407,8 @@
- 2
- - import_load_placeholder_references
- 1
- - import_reassign_placeholder_user_records
- 1
- - import_refresh_import_jid
- 1
- - incident_management

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
1b97d718c83c46dce5429321e7d6ab307a623fd4b22eda6900a21e451b5fdb99

View File

@ -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.

View File

@ -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)
```

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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 ""

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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) { {} }

View File

@ -24,7 +24,6 @@ const defaultConfigHelpUrl =
const provideData = {
kasCheckVersion: '14.8.0',
projectPath: 'path/to/project',
};
const defaultProps = {
agents: clusterAgents,

View File

@ -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,
},
];

View File

@ -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,

View File

@ -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',

View File

@ -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,
});
});

View File

@ -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';

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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],

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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/
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)

View File

@ -0,0 +1 @@
722480422308f4bdba9e5ab874b6d4752667e51a

View File

@ -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"