Commit 08c429d9 authored by Arturo Herrero's avatar Arturo Herrero

Merge branch...

Merge branch '300351-es-migration-add-permission-data-to-notes-attempt-3-as-indexed-json-changes' into 'master'

Delete orphaned notes from Elasticsearch index

See merge request gitlab-org/gitlab!56162
parents ee1e606f 5dcf2e8c
...@@ -57,7 +57,7 @@ module Elastic ...@@ -57,7 +57,7 @@ module Elastic
# protect against missing project and project_feature and set visibility to PRIVATE # protect against missing project and project_feature and set visibility to PRIVATE
# if the project_feature is missing on a project # if the project_feature is missing on a project
def safely_read_project_feature_for_elasticsearch(feature) def safely_read_project_feature_for_elasticsearch(feature)
return unless target.project return ProjectFeature::DISABLED unless target.project
if target.project.project_feature if target.project.project_feature
target.project.project_feature.access_level(feature) target.project.project_feature.access_level(feature)
......
# frozen_string_literal: true
module Elastic
module Latest
class DocumentShouldBeDeletedFromIndexError < StandardError
attr_reader :class_name, :record_id
def initialize(class_name, record_id)
@class_name, @record_id = class_name, record_id
end
def message
"#{class_name} with id #{record_id} should be deleted from the index."
end
end
end
end
...@@ -6,9 +6,15 @@ module Elastic ...@@ -6,9 +6,15 @@ module Elastic
delegate :noteable, to: :target delegate :noteable, to: :target
def as_indexed_json(options = {}) def as_indexed_json(options = {})
# `noteable` can be sometimes be nil (eg. when a commit has been
# deleted) or somehow it was left orphaned in the database. In such
# cases we want to delete it from the index since there is no value in
# having orphaned notes be searchable.
raise Elastic::Latest::DocumentShouldBeDeletedFromIndexError.new(target.class.name, target.id) if noteable.nil?
data = {} data = {}
# We don't use as_json(only: ...) because it calls all virtual and serialized attributtes # We don't use as_json(only: ...) because it calls all virtual and serialized attributes
# https://gitlab.com/gitlab-org/gitlab/issues/349 # https://gitlab.com/gitlab-org/gitlab/issues/349
[:id, :note, :project_id, :noteable_type, :noteable_id, :created_at, :updated_at, :confidential].each do |attr| [:id, :note, :project_id, :noteable_type, :noteable_id, :created_at, :updated_at, :confidential].each do |attr|
data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr) data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr)
...@@ -22,12 +28,11 @@ module Elastic ...@@ -22,12 +28,11 @@ module Elastic
} }
end end
# only attempt to set project permissions if associated to a project
# do not add the permission fields unless the `remove_permissions_data_from_notes_documents` # do not add the permission fields unless the `remove_permissions_data_from_notes_documents`
# migration has completed otherwise the migration will never finish # migration has completed otherwise the migration will never finish
if target.project && Elastic::DataMigrationService.migration_has_finished?(:remove_permissions_data_from_notes_documents) if Elastic::DataMigrationService.migration_has_finished?(:remove_permissions_data_from_notes_documents)
data['visibility_level'] = target.project.visibility_level data['visibility_level'] = target.project&.visibility_level || Gitlab::VisibilityLevel::PRIVATE
merge_project_feature_access_level(data, noteable) merge_project_feature_access_level(data)
end end
data.merge(generic_attributes) data.merge(generic_attributes)
...@@ -35,9 +40,7 @@ module Elastic ...@@ -35,9 +40,7 @@ module Elastic
private private
def merge_project_feature_access_level(data, noteable) def merge_project_feature_access_level(data)
return unless noteable
case noteable case noteable
when Snippet when Snippet
data['snippets_access_level'] = safely_read_project_feature_for_elasticsearch(:snippets) data['snippets_access_level'] = safely_read_project_feature_for_elasticsearch(:snippets)
......
...@@ -63,6 +63,9 @@ module Gitlab ...@@ -63,6 +63,9 @@ module Gitlab
op = build_op(ref, proxy) op = build_op(ref, proxy)
submit(ref, { index: op }, proxy.as_indexed_json) submit(ref, { index: op }, proxy.as_indexed_json)
rescue ::Elastic::Latest::DocumentShouldBeDeletedFromIndexError => error
logger.warn(message: error.message, record_id: error.record_id, class_name: error.class_name)
delete(ref)
end end
def delete(ref) def delete(ref)
......
...@@ -146,6 +146,19 @@ RSpec.describe Gitlab::Elastic::BulkIndexer, :elastic do ...@@ -146,6 +146,19 @@ RSpec.describe Gitlab::Elastic::BulkIndexer, :elastic do
expect(search_one(Issue)).to have_attributes(issue_as_json) expect(search_one(Issue)).to have_attributes(issue_as_json)
end end
it 'deletes the issue from the index if DocumentShouldBeDeletedFromIndexException is raised' do
database_record = issue_as_ref.database_record
allow(database_record.__elasticsearch__)
.to receive(:as_indexed_json)
.and_raise ::Elastic::Latest::DocumentShouldBeDeletedFromIndexError.new(database_record.class.name, database_record.id)
expect(indexer.process(issue_as_ref).flush).to be_empty
refresh_index!
expect(search(Issue, '*').size).to eq(0)
end
end end
context 'deleting an issue' do context 'deleting an issue' do
......
...@@ -121,36 +121,35 @@ RSpec.describe Note, :elastic do ...@@ -121,36 +121,35 @@ RSpec.describe Note, :elastic do
expect(note.__elasticsearch__.as_indexed_json).to eq(expected_hash) expect(note.__elasticsearch__.as_indexed_json).to eq(expected_hash)
end end
it 'does not raise error for notes with null noteable references' do it 'raises Elastic::Latest::DocumentShouldBeDeletedFromIndexError when noteable is nil' do
note = create(:note_on_issue) note = create(:note_on_issue)
allow(note).to receive(:noteable).and_return(nil) allow(note).to receive(:noteable).and_return(nil)
expect { note.__elasticsearch__.as_indexed_json }.not_to raise_error expect { note.__elasticsearch__.as_indexed_json }.to raise_error(::Elastic::Latest::DocumentShouldBeDeletedFromIndexError)
end end
where(:note_type, :project_permission, :access_level) do where(:note_type, :project_feature_permission, :access_level) do
:note_on_issue | ProjectFeature::ENABLED | 'issues_access_level' :note_on_issue | ProjectFeature::ENABLED | 'issues_access_level'
:note_on_project_snippet | ProjectFeature::DISABLED | 'snippets_access_level' :note_on_project_snippet | ProjectFeature::DISABLED | 'snippets_access_level'
:note_on_personal_snippet | nil | false :note_on_personal_snippet | nil | nil
:note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level' :note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level' :note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level'
:diff_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level' :diff_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:diff_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level' :diff_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level'
:diff_note_on_design | ProjectFeature::ENABLED | false :diff_note_on_design | ProjectFeature::ENABLED | nil
:legacy_diff_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level' :legacy_diff_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:legacy_diff_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level' :legacy_diff_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level'
:note_on_alert | ProjectFeature::PRIVATE | false :note_on_alert | ProjectFeature::PRIVATE | nil
:note_on_design | ProjectFeature::ENABLED | false :note_on_design | ProjectFeature::ENABLED | nil
:note_on_epic | nil | false :note_on_epic | nil | nil
:note_on_vulnerability | ProjectFeature::PRIVATE | false :note_on_vulnerability | ProjectFeature::PRIVATE | nil
:discussion_note_on_vulnerability | ProjectFeature::PRIVATE | false :discussion_note_on_vulnerability | ProjectFeature::PRIVATE | nil
:discussion_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level' :discussion_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:discussion_note_on_issue | ProjectFeature::ENABLED | 'issues_access_level' :discussion_note_on_issue | ProjectFeature::ENABLED | 'issues_access_level'
:discussion_note_on_project_snippet | ProjectFeature::DISABLED | 'snippets_access_level' :discussion_note_on_project_snippet | ProjectFeature::DISABLED | 'snippets_access_level'
:discussion_note_on_personal_snippet | nil | false :discussion_note_on_personal_snippet | nil | nil
:note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:discussion_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level' :discussion_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level'
:track_mr_picking_note | ProjectFeature::PUBLIC | 'merge_requests_access_level' :track_mr_picking_note | nil | nil
end end
with_them do with_them do
...@@ -160,7 +159,7 @@ RSpec.describe Note, :elastic do ...@@ -160,7 +159,7 @@ RSpec.describe Note, :elastic do
let(:note_json) { note.__elasticsearch__.as_indexed_json } let(:note_json) { note.__elasticsearch__.as_indexed_json }
before do before do
project.project_feature.update_attribute(access_level.to_sym, project_permission) if access_level.present? project.project_feature.update_attribute(access_level.to_sym, project_feature_permission) if access_level.present?
end end
it 'does not contain permissions if remove_permissions_data_from_notes_documents is not finished' do it 'does not contain permissions if remove_permissions_data_from_notes_documents is not finished' do
...@@ -175,14 +174,24 @@ RSpec.describe Note, :elastic do ...@@ -175,14 +174,24 @@ RSpec.describe Note, :elastic do
it 'contains the correct permissions', :aggregate_failures do it 'contains the correct permissions', :aggregate_failures do
if access_level if access_level
expect(note_json).to have_key(access_level) expect(note_json).to have_key(access_level)
expect(note_json[access_level]).to eq(project_permission) expect(note_json[access_level]).to eq(project_feature_permission)
end
expected_visibility_level = project&.visibility_level || Gitlab::VisibilityLevel::PRIVATE
expect(note_json).to have_key('visibility_level')
expect(note_json['visibility_level']).to eq(expected_visibility_level)
end
context 'when the project does not exist' do
before do
note.project = nil
end end
if project_permission it 'has DISABLED access_level' do
expect(note_json).to have_key('visibility_level') if access_level
expect(note_json['visibility_level']).to eq(project.visibility_level) expect(note_json).to have_key(access_level)
else expect(note_json[access_level]).to eq(ProjectFeature::DISABLED)
expect(note_json).not_to have_key('visibility_level') end
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment