From 09c11729be157f9c41f59108dc24d177f5ff54bc Mon Sep 17 00:00:00 2001 From: Mario de la Ossa Date: Tue, 5 Mar 2019 16:28:11 -0600 Subject: [PATCH 1/5] Enable elasticsearch per project or group In order to allow us to incrementally enable ES on gitlab.com we add two new classes: - ElasticsearchIndexedNamespaces - ElasticsearchIndexedProjects These classes are used by ApplicationSetting to enable/disable projects and namespaces (groups) that should be indexed by elasticsearch. We also have the application setting, `elasticsearch_limit_indexing`, that enables/disables the new functionality In order to be able to selectively choose projects/namespaces to use with elasticsearch, `elasticsearch_limit_indexing` MUST be enabled under the admin integrations options. --- app/controllers/search_controller.rb | 2 - app/helpers/search_helper.rb | 4 + app/services/search_service.rb | 2 + app/views/search/_category.html.haml | 2 +- app/views/search/_form.html.haml | 4 +- config/sidekiq_queues.yml | 1 + db/schema.rb | 17 ++++ .../integrations/index.js | 88 +++++++++++++++++++ .../helpers/ee/application_settings_helper.rb | 15 ++++ .../concerns/elastic/application_search.rb | 6 +- .../concerns/elastic/projects_search.rb | 4 + .../concerns/elastic/repositories_search.rb | 2 +- .../concerns/elastic/snippets_search.rb | 4 + .../elastic/wiki_repositories_search.rb | 2 +- ee/app/models/ee/application_setting.rb | 58 ++++++++++++ ee/app/models/ee/issue_assignee.rb | 2 +- ee/app/models/ee/namespace.rb | 4 + ee/app/models/ee/note.rb | 3 +- ee/app/models/ee/project_feature.rb | 2 +- ee/app/models/ee/project_wiki.rb | 2 +- .../models/elasticsearch_indexed_namespace.rb | 28 ++++++ .../models/elasticsearch_indexed_project.rb | 38 ++++++++ ee/app/models/index_status.rb | 2 + ee/app/services/ee/git/branch_push_service.rb | 6 +- ee/app/services/ee/git_push_service.rb | 31 +++++++ ee/app/services/ee/search/global_service.rb | 7 +- ee/app/services/ee/search/group_service.rb | 7 ++ ee/app/services/ee/search/project_service.rb | 7 +- ee/app/services/ee/search/snippet_service.rb | 7 +- ee/app/services/ee/search_service.rb | 11 +++ .../_elasticsearch_form.html.haml | 14 +++ ee/app/workers/all_queues.yml | 1 + ee/app/workers/ee/post_receive.rb | 3 +- .../elastic_batch_project_indexer_worker.rb | 2 + .../workers/elastic_commit_indexer_worker.rb | 2 + ee/app/workers/elastic_indexer_worker.rb | 32 +++++-- .../elastic_namespace_indexer_worker.rb | 39 ++++++++ .../1607-elastic_feature_flag_per_group.yml | 5 ++ ...h_limit_indexing_to_application_setting.rb | 20 +++++ ...namespace_link_and_elastic_project_link.rb | 24 +++++ ee/lib/ee/gitlab/fake_application_settings.rb | 15 ++++ ee/lib/tasks/gitlab/elastic.rake | 2 +- .../elasticsearch_indexed_containers.rb | 11 +++ ee/spec/features/admin/admin_settings_spec.rb | 63 +++++++++++-- ee/spec/models/application_setting_spec.rb | 73 +++++++++++++++ ee/spec/models/concerns/elastic/issue_spec.rb | 46 ++++++++++ .../concerns/elastic/merge_request_spec.rb | 9 ++ .../models/concerns/elastic/milestone_spec.rb | 9 ++ ee/spec/models/concerns/elastic/note_spec.rb | 29 ++++++ .../models/concerns/elastic/project_spec.rb | 87 ++++++++++++++++++ .../models/concerns/elastic/snippet_spec.rb | 10 +++ ee/spec/models/ee/namespace_spec.rb | 26 ++++++ .../elasticsearch_indexed_namespace_spec.rb | 20 +++++ .../elasticsearch_indexed_project_spec.rb | 20 +++++ ee/spec/models/project_import_state_spec.rb | 6 +- .../ee/git/branch_push_service_spec.rb | 41 +++++++++ .../concerns/elastic/limited_indexing.rb | 41 +++++++++ .../models/elasticsearch_indexed_container.rb | 45 ++++++++++ ...astic_batch_project_indexer_worker_spec.rb | 26 +++++- .../workers/elastic_indexer_worker_spec.rb | 27 ++++++ .../elastic_namespace_indexer_worker_spec.rb | 47 ++++++++++ ee/spec/workers/post_receive_spec.rb | 50 +++++++++++ lib/gitlab/fake_application_settings.rb | 2 + locale/gitlab.pot | 18 ++++ 64 files changed, 1195 insertions(+), 38 deletions(-) create mode 100644 ee/app/assets/javascripts/pages/admin/application_settings/integrations/index.js create mode 100644 ee/app/models/elasticsearch_indexed_namespace.rb create mode 100644 ee/app/models/elasticsearch_indexed_project.rb create mode 100644 ee/app/services/ee/git_push_service.rb create mode 100644 ee/app/services/ee/search_service.rb create mode 100644 ee/app/workers/elastic_namespace_indexer_worker.rb create mode 100644 ee/changelogs/unreleased/1607-elastic_feature_flag_per_group.yml create mode 100644 ee/db/migrate/20190318020549_add_elasticsearch_limit_indexing_to_application_setting.rb create mode 100644 ee/db/migrate/20190318021429_add_elastic_namespace_link_and_elastic_project_link.rb create mode 100644 ee/lib/ee/gitlab/fake_application_settings.rb create mode 100644 ee/spec/factories/elasticsearch_indexed_containers.rb create mode 100644 ee/spec/models/elasticsearch_indexed_namespace_spec.rb create mode 100644 ee/spec/models/elasticsearch_indexed_project_spec.rb create mode 100644 ee/spec/support/shared_examples/models/concerns/elastic/limited_indexing.rb create mode 100644 ee/spec/support/shared_examples/models/elasticsearch_indexed_container.rb create mode 100644 ee/spec/workers/elastic_namespace_indexer_worker_spec.rb diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 90d4bc674d9a63..a80ab3bcd2876e 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -14,8 +14,6 @@ class SearchController < ApplicationController layout 'search' def show - search_service = SearchService.new(current_user, params) - @project = search_service.project @group = search_service.group diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 316f05d349c07b..72c3a860c61337 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -50,6 +50,10 @@ def search_blob_title(project, filename) filename end + def search_service + @search_service ||= ::SearchService.new(current_user, params) + end + private # Autocomplete results for various settings pages diff --git a/app/services/search_service.rb b/app/services/search_service.rb index e0cbfac242059e..919a7c2a4d928b 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -69,3 +69,5 @@ def search_service attr_reader :current_user, :params end + +SearchService.prepend(EE::SearchService) diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 1958224ad85457..bb8afa73e544aa 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -87,7 +87,7 @@ = _("Milestones") %span.badge.badge-pill = limited_count(@search_results.limited_milestones_count) - - if Gitlab::CurrentSettings.elasticsearch_search? + - if search_service.use_elasticsearch? %li{ class: active_when(@scope == 'blobs') } = link_to search_filter_path(scope: 'blobs') do = _("Code") diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index e6162d56bbebed..048240cd39ea05 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -13,7 +13,7 @@ - unless params[:snippets].eql? 'true' = render 'filter' = button_tag _("Search"), class: "btn btn-success btn-search" - - if Gitlab::CurrentSettings.elasticsearch_search? + - if search_service.use_elasticsearch? .form-text.text-muted - = link_to 'Advanced search functionality', help_page_path('user/search/advanced_search_syntax.md'), target: '_blank' + = link_to _('Advanced search functionality'), help_page_path('user/search/advanced_search_syntax.md'), target: '_blank' is enabled. diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 3d569a020b7ffc..d0a56a0d3ea105 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -103,6 +103,7 @@ - [elastic_batch_project_indexer, 1] - [elastic_indexer, 1] - [elastic_commit_indexer, 1] + - [elastic_namespace_indexer, 1] - [export_csv, 1] - [incident_management, 2] diff --git a/db/schema.rb b/db/schema.rb index fe35987ac467f7..4b9433a1092d46 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -217,6 +217,7 @@ t.string "runners_registration_token_encrypted" t.integer "local_markdown_version", default: 0, null: false t.integer "first_day_of_week", default: 0, null: false + t.boolean "elasticsearch_limit_indexing", default: false, null: false t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree @@ -1065,6 +1066,20 @@ t.index ["merge_request_id"], name: "index_draft_notes_on_merge_request_id", using: :btree end + create_table "elasticsearch_indexed_namespaces", id: false, force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "namespace_id" + t.index ["namespace_id"], name: "index_elasticsearch_indexed_namespaces_on_namespace_id", unique: true, using: :btree + end + + create_table "elasticsearch_indexed_projects", id: false, force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "project_id" + t.index ["project_id"], name: "index_elasticsearch_indexed_projects_on_project_id", unique: true, using: :btree + end + create_table "emails", force: :cascade do |t| t.integer "user_id", null: false t.string "email", null: false @@ -3456,6 +3471,8 @@ add_foreign_key "design_management_versions", "design_management_designs", on_delete: :cascade add_foreign_key "draft_notes", "merge_requests", on_delete: :cascade add_foreign_key "draft_notes", "users", column: "author_id", on_delete: :cascade + add_foreign_key "elasticsearch_indexed_namespaces", "namespaces", on_delete: :cascade + add_foreign_key "elasticsearch_indexed_projects", "projects", on_delete: :cascade add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade add_foreign_key "epic_issues", "epics", on_delete: :cascade add_foreign_key "epic_issues", "issues", on_delete: :cascade diff --git a/ee/app/assets/javascripts/pages/admin/application_settings/integrations/index.js b/ee/app/assets/javascripts/pages/admin/application_settings/integrations/index.js new file mode 100644 index 00000000000000..3b736a52d89c38 --- /dev/null +++ b/ee/app/assets/javascripts/pages/admin/application_settings/integrations/index.js @@ -0,0 +1,88 @@ +import $ from 'jquery'; +import { s__ } from '~/locale'; +import '~/flash'; +import Api from '~/api'; + +const onLimitCheckboxChange = function onLimitCheckboxChange( + e, + $limitByNamespaces, + $limitByProjects, +) { + const $namespacesSelect = $('.select2', $limitByNamespaces); + const $projectsSelect = $('.select2', $limitByNamespaces); + + $namespacesSelect.select2('data', null); + $projectsSelect.select2('data', null); + $limitByNamespaces.toggleClass('hidden', !e.currentTarget.checked); + $limitByProjects.toggleClass('hidden', !e.currentTarget.checked); +}; + +export default function elasticsearchForm() { + const $container = $('#js-elasticsearch-settings'); + const $limitCheckbox = $('.js-limit-checkbox', $container); + const $limitByNamespaces = $('.js-limit-namespaces', $container); + const $limitByProjects = $('.js-limit-projects', $container); + const $namespacesDropdown = $('.js-elasticsearch-namespaces', $container); + const $projectsDropdown = $('.js-elasticsearch-projects', $container); + + $limitCheckbox.on('change', e => onLimitCheckboxChange(e, $limitByNamespaces, $limitByProjects)); + + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(() => { + $namespacesDropdown.select2({ + placeholder: s__('Elastic|Select groups to enable.'), + multiple: true, + initSelection($el, callback) { + callback($el.data('selected')); + }, + ajax: { + url: Api.buildUrl(Api.groupsPath), + dataType: 'JSON', + quietMillis: 250, + data(search) { + return { + search, + }; + }, + results(data) { + return { + results: data.map(group => ({ + id: group.id, + text: group.full_name, + })), + }; + }, + }, + }); + $projectsDropdown.select2({ + placeholder: s__('Elastic|Select projects to enable.'), + multiple: true, + initSelection($el, callback) { + callback($el.data('selected')); + }, + ajax: { + url: Api.buildUrl(Api.projectsPath), + dataType: 'JSON', + quietMillis: 250, + data(search) { + return { + search, + }; + }, + results(data) { + return { + results: data.map(project => ({ + id: project.id, + text: project.full_name, + })), + }; + }, + }, + }); + }) + .catch(() => {}); +} + +document.addEventListener('DOMContentLoaded', () => { + elasticsearchForm(); +}); diff --git a/ee/app/helpers/ee/application_settings_helper.rb b/ee/app/helpers/ee/application_settings_helper.rb index bc90d6f9637c16..26e60587f921fc 100644 --- a/ee/app/helpers/ee/application_settings_helper.rb +++ b/ee/app/helpers/ee/application_settings_helper.rb @@ -62,6 +62,9 @@ def visible_attributes :elasticsearch_indexing, :elasticsearch_search, :elasticsearch_url, + :elasticsearch_limit_indexing, + :elasticsearch_namespace_ids, + :elasticsearch_project_ids, :geo_status_timeout, :help_text, :pseudonymizer_enabled, @@ -78,6 +81,18 @@ def visible_attributes ] end + def elasticsearch_objects_options(objects) + objects.map { |g| { id: g.id, text: g.full_name } } + end + + def elasticsearch_namespace_ids + ElasticsearchIndexedNamespace.namespace_ids.join(',') + end + + def elasticsearch_project_ids + ElasticsearchIndexedProject.project_ids.join(',') + end + def self.repository_mirror_attributes [ :mirror_max_capacity, diff --git a/ee/app/models/concerns/elastic/application_search.rb b/ee/app/models/concerns/elastic/application_search.rb index 5222b0c2dad676..c058ad4c5b0b7a 100644 --- a/ee/app/models/concerns/elastic/application_search.rb +++ b/ee/app/models/concerns/elastic/application_search.rb @@ -228,7 +228,11 @@ module ApplicationSearch # Should be overridden in the models where some records should be skipped def searchable? - true + self.use_elasticsearch? + end + + def use_elasticsearch? + self.project&.use_elasticsearch? end def generic_attributes diff --git a/ee/app/models/concerns/elastic/projects_search.rb b/ee/app/models/concerns/elastic/projects_search.rb index 70f353439885c4..00b68e17ae20f6 100644 --- a/ee/app/models/concerns/elastic/projects_search.rb +++ b/ee/app/models/concerns/elastic/projects_search.rb @@ -15,6 +15,10 @@ module ProjectsSearch included do include ApplicationSearch + def use_elasticsearch? + ::Gitlab::CurrentSettings.elasticsearch_indexes_project?(self) + end + def as_indexed_json(options = {}) # We don't use as_json(only: ...) because it calls all virtual and serialized attributtes # https://gitlab.com/gitlab-org/gitlab-ee/issues/349 diff --git a/ee/app/models/concerns/elastic/repositories_search.rb b/ee/app/models/concerns/elastic/repositories_search.rb index 7806d9d65b82f3..d6b441e8b69137 100644 --- a/ee/app/models/concerns/elastic/repositories_search.rb +++ b/ee/app/models/concerns/elastic/repositories_search.rb @@ -25,7 +25,7 @@ def client_for_indexing def self.import Project.find_each do |project| - if project.repository.exists? && !project.repository.empty? + if project.repository.exists? && !project.repository.empty? && project.use_elasticsearch? project.repository.index_commits project.repository.index_blobs end diff --git a/ee/app/models/concerns/elastic/snippets_search.rb b/ee/app/models/concerns/elastic/snippets_search.rb index ff9b8059b122d8..0cf4ea7f3bca92 100644 --- a/ee/app/models/concerns/elastic/snippets_search.rb +++ b/ee/app/models/concerns/elastic/snippets_search.rb @@ -32,6 +32,10 @@ def as_indexed_json(options = {}) data end + def use_elasticsearch? + ::Gitlab::CurrentSettings.elasticsearch_indexing? + end + def self.elastic_search(query, options: {}) query_hash = basic_query_hash(%w(title file_name), query) diff --git a/ee/app/models/concerns/elastic/wiki_repositories_search.rb b/ee/app/models/concerns/elastic/wiki_repositories_search.rb index a14403d767660c..b24a06694fe40d 100644 --- a/ee/app/models/concerns/elastic/wiki_repositories_search.rb +++ b/ee/app/models/concerns/elastic/wiki_repositories_search.rb @@ -25,7 +25,7 @@ def client_for_indexing def self.import Project.with_wiki_enabled.find_each do |project| - unless project.wiki.empty? + if project.use_elasticsearch? && !project.wiki.empty? project.wiki.index_blobs end end diff --git a/ee/app/models/ee/application_setting.rb b/ee/app/models/ee/application_setting.rb index 23e2d0f50d58c5..5dca3a5af3a0d0 100644 --- a/ee/app/models/ee/application_setting.rb +++ b/ee/app/models/ee/application_setting.rb @@ -14,6 +14,11 @@ module ApplicationSetting EMAIL_ADDITIONAL_TEXT_CHARACTER_LIMIT = 10_000 INSTANCE_REVIEW_MIN_USERS = 100 + attr_accessor :elasticsearch_namespace_ids, :elasticsearch_project_ids + + after_save -> { update_elasticsearch_containers(ElasticsearchIndexedNamespace, :namespace_id, elasticsearch_namespace_ids) }, on: [:create, :update] + after_save -> { update_elasticsearch_containers(ElasticsearchIndexedProject, :project_id, elasticsearch_project_ids) }, on: [:create, :update] + belongs_to :file_template_project, class_name: "Project" ignore_column :minimum_mirror_sync_time @@ -120,6 +125,59 @@ def defaults end end + def update_elasticsearch_containers(klass, attribute, container_ids) + return unless elasticsearch_limit_indexing? + + container_ids = container_ids&.split(",") + return unless container_ids.present? + + # Destroy any containers that have been removed. This runs callbacks, etc + # #rubocop:disable Cop/DestroyAll + klass.where.not(attribute => container_ids).each_batch do |batch, _index| + batch.destroy_all + end + # #rubocop:enable Cop/DestroyAll + + # Disregard any duplicates that are already present + container_ids -= klass.pluck(attribute) + + # Add new containers + container_ids.each { |id| klass.create(attribute => id) } + end + + def elasticsearch_indexes_project?(project) + return false unless elasticsearch_indexing? + return true unless elasticsearch_limit_indexing? + + elasticsearch_limited_projects.exists?(project.id) + end + + def elasticsearch_indexes_namespace?(namespace) + return false unless elasticsearch_indexing? + return true unless elasticsearch_limit_indexing? + + elasticsearch_limited_namespaces.exists?(namespace.id) + end + + def elasticsearch_limited_projects(ignore_namespaces = false) + return ::Project.where(id: ElasticsearchIndexedProject.select(:project_id)) if ignore_namespaces + + union = ::Gitlab::SQL::Union.new([ + ::Project.where(namespace_id: elasticsearch_limited_namespaces.select(:id)), + ::Project.where(id: ElasticsearchIndexedProject.select(:project_id)) + ]).to_sql + + ::Project.from("(#{union}) projects") + end + + def elasticsearch_limited_namespaces(ignore_descendants = false) + namespaces = ::Namespace.where(id: ElasticsearchIndexedNamespace.select(:namespace_id)) + + return namespaces if ignore_descendants + + ::Gitlab::ObjectHierarchy.new(namespaces).base_and_descendants + end + def pseudonymizer_available? License.feature_available?(:pseudonymizer) end diff --git a/ee/app/models/ee/issue_assignee.rb b/ee/app/models/ee/issue_assignee.rb index 3892ae606c94ed..74076782096ab0 100644 --- a/ee/app/models/ee/issue_assignee.rb +++ b/ee/app/models/ee/issue_assignee.rb @@ -8,7 +8,7 @@ module IssueAssignee end def update_elasticsearch_index - if ::Gitlab::CurrentSettings.current_application_settings.elasticsearch_indexing? + if issue.project&.use_elasticsearch? ::ElasticIndexerWorker.perform_async( :update, 'Issue', diff --git a/ee/app/models/ee/namespace.rb b/ee/app/models/ee/namespace.rb index 91570b4d3d5b2f..c1cabaa4c460aa 100644 --- a/ee/app/models/ee/namespace.rb +++ b/ee/app/models/ee/namespace.rb @@ -271,6 +271,10 @@ def gold_plan? actual_plan_name == GOLD_PLAN end + def use_elasticsearch? + ::Gitlab::CurrentSettings.elasticsearch_indexes_namespace?(self) + end + private def validate_plan_name diff --git a/ee/app/models/ee/note.rb b/ee/app/models/ee/note.rb index 902f8a9772d410..5dcaf0e8391089 100644 --- a/ee/app/models/ee/note.rb +++ b/ee/app/models/ee/note.rb @@ -14,8 +14,9 @@ module Note scope :searchable, -> { where(system: false) } end + # Original method in Elastic::ApplicationSearch def searchable? - !system + !system && super end def for_epic? diff --git a/ee/app/models/ee/project_feature.rb b/ee/app/models/ee/project_feature.rb index cb5e4ee998ce20..f2b7e39e9b89a7 100644 --- a/ee/app/models/ee/project_feature.rb +++ b/ee/app/models/ee/project_feature.rb @@ -6,7 +6,7 @@ module ProjectFeature prepended do after_commit on: :update do - if ::Gitlab::CurrentSettings.current_application_settings.elasticsearch_indexing? + if project.use_elasticsearch? ElasticIndexerWorker.perform_async(:update, 'Project', project_id, project.es_id) end end diff --git a/ee/app/models/ee/project_wiki.rb b/ee/app/models/ee/project_wiki.rb index be37ff778b0c48..e240dafe2ec453 100644 --- a/ee/app/models/ee/project_wiki.rb +++ b/ee/app/models/ee/project_wiki.rb @@ -16,7 +16,7 @@ def kerberos_url_to_repo end def update_elastic_index - index_blobs if ::Gitlab::CurrentSettings.elasticsearch_indexing? + index_blobs if project.use_elasticsearch? end def path_to_repo diff --git a/ee/app/models/elasticsearch_indexed_namespace.rb b/ee/app/models/elasticsearch_indexed_namespace.rb new file mode 100644 index 00000000000000..647c7ec8754172 --- /dev/null +++ b/ee/app/models/elasticsearch_indexed_namespace.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class ElasticsearchIndexedNamespace < ActiveRecord::Base + include EachBatch + + self.primary_key = 'namespace_id' + + after_commit :index, on: :create + after_commit :delete_from_index, on: :destroy + + belongs_to :namespace + + validates :namespace_id, presence: true, uniqueness: true + + def self.namespace_ids + self.pluck(:namespace_id) + end + + private + + def index + ElasticNamespaceIndexerWorker.perform_async(namespace_id, :index) + end + + def delete_from_index + ElasticNamespaceIndexerWorker.perform_async(namespace_id, :delete) + end +end diff --git a/ee/app/models/elasticsearch_indexed_project.rb b/ee/app/models/elasticsearch_indexed_project.rb new file mode 100644 index 00000000000000..4d6555147e9f53 --- /dev/null +++ b/ee/app/models/elasticsearch_indexed_project.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class ElasticsearchIndexedProject < ActiveRecord::Base + include EachBatch + + self.primary_key = 'project_id' + + after_commit :index, on: :create + after_commit :delete_from_index, on: :destroy + + belongs_to :project + + validates :project_id, presence: true, uniqueness: true + + def self.project_ids + self.pluck(:project_id) + end + + private + + def index + if Gitlab::CurrentSettings.elasticsearch_indexing? && project.searchable? + ElasticIndexerWorker.perform_async(:index, project.class.to_s, project.id, project.es_id) + end + end + + def delete_from_index + if Gitlab::CurrentSettings.elasticsearch_indexing? && project.searchable? + ElasticIndexerWorker.perform_async( + :delete, + project.class.to_s, + project.id, + project.es_id, + es_parent: project.es_parent + ) + end + end +end diff --git a/ee/app/models/index_status.rb b/ee/app/models/index_status.rb index eca80759f7af0a..ee6ff2e6193f32 100644 --- a/ee/app/models/index_status.rb +++ b/ee/app/models/index_status.rb @@ -4,4 +4,6 @@ class IndexStatus < ActiveRecord::Base belongs_to :project validates :project_id, uniqueness: true, presence: true + + scope :for_project, ->(project_id) { where(project_id: project_id) } end diff --git a/ee/app/services/ee/git/branch_push_service.rb b/ee/app/services/ee/git/branch_push_service.rb index 2b7671c80b43bc..4557335fcd1e99 100644 --- a/ee/app/services/ee/git/branch_push_service.rb +++ b/ee/app/services/ee/git/branch_push_service.rb @@ -9,7 +9,7 @@ module BranchPushService override :execute_related_hooks def execute_related_hooks - if ::Gitlab::CurrentSettings.elasticsearch_indexing? && default_branch? && should_index_commits? + if should_index_commits? ::ElasticCommitIndexerWorker.perform_async(project.id, params[:oldrev], params[:newrev]) end @@ -19,7 +19,9 @@ def execute_related_hooks private def should_index_commits? - ::Gitlab::Redis::SharedState.with { |redis| !redis.sismember(:elastic_projects_indexing, project.id) } + default_branch? && + project.use_elasticsearch? && + ::Gitlab::Redis::SharedState.with { |redis| !redis.sismember(:elastic_projects_indexing, project.id) } end override :pipeline_options diff --git a/ee/app/services/ee/git_push_service.rb b/ee/app/services/ee/git_push_service.rb new file mode 100644 index 00000000000000..b0508fd6f637ae --- /dev/null +++ b/ee/app/services/ee/git_push_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module EE + module GitPushService + extend ::Gitlab::Utils::Override + + protected + + override :execute_related_hooks + def execute_related_hooks + if should_index_commits? + ::ElasticCommitIndexerWorker.perform_async(project.id, params[:oldrev], params[:newrev]) + end + + super + end + + private + + def should_index_commits? + default_branch? && + project.use_elasticsearch? && + ::Gitlab::Redis::SharedState.with { |redis| !redis.sismember(:elastic_projects_indexing, project.id) } + end + + override :pipeline_options + def pipeline_options + { mirror_update: project.mirror? && project.repository.up_to_date_with_upstream?(branch_name) } + end + end +end diff --git a/ee/app/services/ee/search/global_service.rb b/ee/app/services/ee/search/global_service.rb index 86a9a3ef30bfdc..b70e4b6b3f11f6 100644 --- a/ee/app/services/ee/search/global_service.rb +++ b/ee/app/services/ee/search/global_service.rb @@ -8,7 +8,7 @@ module GlobalService override :execute def execute - if ::Gitlab::CurrentSettings.elasticsearch_search? + if use_elasticsearch? ::Gitlab::Elastic::SearchResults.new(current_user, params[:search], elastic_projects, projects, elastic_global) @@ -17,6 +17,11 @@ def execute end end + def use_elasticsearch? + ::Gitlab::CurrentSettings.elasticsearch_search? && + !::Gitlab::CurrentSettings.elasticsearch_limit_indexing? + end + def elastic_projects strong_memoize(:elastic_projects) do if current_user&.full_private_access? diff --git a/ee/app/services/ee/search/group_service.rb b/ee/app/services/ee/search/group_service.rb index d8ca6698891d3e..2635b7f6c6db5e 100644 --- a/ee/app/services/ee/search/group_service.rb +++ b/ee/app/services/ee/search/group_service.rb @@ -5,10 +5,17 @@ module Search module GroupService extend ::Gitlab::Utils::Override + override :use_elasticsearch? + def use_elasticsearch? + group&.use_elasticsearch? + end + + override :elastic_projects def elastic_projects @elastic_projects ||= projects.pluck(:id) # rubocop:disable CodeReuse/ActiveRecord end + override :elastic_global def elastic_global false end diff --git a/ee/app/services/ee/search/project_service.rb b/ee/app/services/ee/search/project_service.rb index 38f2b0dad8cd13..8bce7d4cdcc2c5 100644 --- a/ee/app/services/ee/search/project_service.rb +++ b/ee/app/services/ee/search/project_service.rb @@ -7,7 +7,7 @@ module ProjectService override :execute def execute - if ::Gitlab::CurrentSettings.elasticsearch_search? + if use_elasticsearch? ::Gitlab::Elastic::ProjectSearchResults.new(current_user, params[:search], project.id, @@ -16,6 +16,11 @@ def execute super end end + + # This method is used in the top-level SearchService, so cannot be in-lined into #execute + def use_elasticsearch? + project.use_elasticsearch? + end end end end diff --git a/ee/app/services/ee/search/snippet_service.rb b/ee/app/services/ee/search/snippet_service.rb index dd343bb4607cfb..e2dbd99dbdf007 100644 --- a/ee/app/services/ee/search/snippet_service.rb +++ b/ee/app/services/ee/search/snippet_service.rb @@ -8,12 +8,17 @@ module SnippetService override :execute def execute - if ::Gitlab::CurrentSettings.elasticsearch_search? + if use_elasticsearch? ::Gitlab::Elastic::SnippetSearchResults.new(current_user, params[:search]) else super end end + + # This method is used in the top-level SearchService, so cannot be in-lined into #execute + def use_elasticsearch? + ::Gitlab::CurrentSettings.elasticsearch_search? + end end end end diff --git a/ee/app/services/ee/search_service.rb b/ee/app/services/ee/search_service.rb new file mode 100644 index 00000000000000..ee238430a896ea --- /dev/null +++ b/ee/app/services/ee/search_service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module EE + module SearchService + # This is a proper method instead of a `delegate` in order to + # avoid adding unnecessary methods to Search::SnippetService + def use_elasticsearch? + search_service.use_elasticsearch? + end + end +end diff --git a/ee/app/views/admin/application_settings/_elasticsearch_form.html.haml b/ee/app/views/admin/application_settings/_elasticsearch_form.html.haml index 73930c941ab491..227cf81728bfd4 100644 --- a/ee/app/views/admin/application_settings/_elasticsearch_form.html.haml +++ b/ee/app/views/admin/application_settings/_elasticsearch_form.html.haml @@ -42,6 +42,20 @@ .form-text.text-muted The url to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://localhost:9200, http://localhost:9201"). + .form-group + .form-check + = f.check_box :elasticsearch_limit_indexing, class: 'form-check-input js-limit-checkbox' + = f.label :elasticsearch_limit_indexing, class: 'form-check-label' do + = _('Limit namespaces and projects that can be indexed') + + .form-group.js-limit-namespaces{ class: ('hidden' unless @application_setting.elasticsearch_limit_indexing) } + = f.label :elasticsearch_namespace_ids, _('Namespaces to index'), class: 'label-bold' + = f.text_field :elasticsearch_namespace_ids, class: 'js-elasticsearch-namespaces form-control', value: elasticsearch_namespace_ids, data: { selected: elasticsearch_objects_options(@application_setting.elasticsearch_limited_namespaces(true)).to_json } + + .form-group.js-limit-projects{ class: ('hidden' unless @application_setting.elasticsearch_limit_indexing) } + = f.label :elasticsearch_project_ids, _('Projects to index'), class: 'label-bold' + = f.text_field :elasticsearch_project_ids, class: 'js-elasticsearch-projects form-control', value: elasticsearch_project_ids, data: { selected: elasticsearch_objects_options(@application_setting.elasticsearch_limited_projects(true)).to_json } + .sub-section %h4 Elasticsearch AWS IAM credentials .form-group diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml index 37514017ee96bb..0371be00eee096 100644 --- a/ee/app/workers/all_queues.yml +++ b/ee/app/workers/all_queues.yml @@ -50,6 +50,7 @@ - admin_emails - create_github_webhook - elastic_batch_project_indexer +- elastic_namespace_indexer - elastic_commit_indexer - elastic_indexer - export_csv diff --git a/ee/app/workers/ee/post_receive.rb b/ee/app/workers/ee/post_receive.rb index 9e4d488aceebdd..600bad4a3da21a 100644 --- a/ee/app/workers/ee/post_receive.rb +++ b/ee/app/workers/ee/post_receive.rb @@ -29,8 +29,7 @@ def process_wiki_changes(post_received) end def update_wiki_es_indexes(post_received) - return unless ::Gitlab::CurrentSettings.current_application_settings - .elasticsearch_indexing? + return unless post_received.project.use_elasticsearch? post_received.project.wiki.index_blobs end diff --git a/ee/app/workers/elastic_batch_project_indexer_worker.rb b/ee/app/workers/elastic_batch_project_indexer_worker.rb index 162b9f420325af..d1f1120396d739 100644 --- a/ee/app/workers/elastic_batch_project_indexer_worker.rb +++ b/ee/app/workers/elastic_batch_project_indexer_worker.rb @@ -19,6 +19,8 @@ def perform(start, finish, update_index = false) private def run_indexer(project, update_index) + return unless project.use_elasticsearch? + # Ensure we remove the hold on the project, no matter what, so ElasticCommitIndexerWorker can do its thing # We do this before the indexer starts to avoid the possibility of pushes coming in during this time not # being indexed. diff --git a/ee/app/workers/elastic_commit_indexer_worker.rb b/ee/app/workers/elastic_commit_indexer_worker.rb index f46093f3f7c90d..e8547568688c68 100644 --- a/ee/app/workers/elastic_commit_indexer_worker.rb +++ b/ee/app/workers/elastic_commit_indexer_worker.rb @@ -10,6 +10,8 @@ def perform(project_id, oldrev = nil, newrev = nil) project = Project.find(project_id) + return true unless project.use_elasticsearch? + Gitlab::Elastic::Indexer.new(project).run(oldrev, newrev) end end diff --git a/ee/app/workers/elastic_indexer_worker.rb b/ee/app/workers/elastic_indexer_worker.rb index aabb0369340dbc..60ba61c8695e89 100644 --- a/ee/app/workers/elastic_indexer_worker.rb +++ b/ee/app/workers/elastic_indexer_worker.rb @@ -17,11 +17,9 @@ def perform(operation, class_name, record_id, es_id, options = {}) record = klass.find(record_id) record.__elasticsearch__.client = client - if klass.nested? - record.__elasticsearch__.__send__ "#{operation}_document", routing: record.es_parent # rubocop:disable GitlabSecurity/PublicSend - else - record.__elasticsearch__.__send__ "#{operation}_document" # rubocop:disable GitlabSecurity/PublicSend - end + import(operation, record, klass) + + initial_index_project(record) if klass == Project && operation.to_s.match?(/index/) update_issue_notes(record, options["changed_fields"]) if klass == Issue when /delete/ @@ -57,6 +55,30 @@ def update_issue_notes(record, changed_fields) def clear_project_data(record_id, es_id) remove_children_documents('project', record_id, es_id) + IndexStatus.for_project(record_id).delete_all + end + + def initial_index_project(project) + { + Issue => project.issues, + MergeRequest => project.merge_requests, + Snippet => project.snippets, + Note => project.notes.searchable, + Milestone => project.milestones + }.each do |klass, objects| + objects.find_each { |object| import(:index, object, klass) } + end + + # Finally, index blobs/commits/wikis + ElasticCommitIndexerWorker.perform_async(project.id) + end + + def import(operation, record, klass) + if klass.nested? + record.__elasticsearch__.__send__ "#{operation}_document", routing: record.es_parent # rubocop:disable GitlabSecurity/PublicSend + else + record.__elasticsearch__.__send__ "#{operation}_document" # rubocop:disable GitlabSecurity/PublicSend + end end def remove_documents_by_project_id(record_id) diff --git a/ee/app/workers/elastic_namespace_indexer_worker.rb b/ee/app/workers/elastic_namespace_indexer_worker.rb new file mode 100644 index 00000000000000..a493c53927556f --- /dev/null +++ b/ee/app/workers/elastic_namespace_indexer_worker.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class ElasticNamespaceIndexerWorker + include ApplicationWorker + + sidekiq_options retry: 2 + + def perform(namespace_id, operation) + return true unless Gitlab::CurrentSettings.elasticsearch_indexing? + return true unless Gitlab::CurrentSettings.elasticsearch_limit_indexing? + + namespace = Namespace.find(namespace_id) + + case operation.to_s + when /index/ + index_projects(namespace) + when /delete/ + delete_from_index(namespace) + end + end + + private + + def index_projects(namespace) + # The default of 1000 is good for us since Sidekiq documentation doesn't recommend more than 1000 per batch call + # https://www.rubydoc.info/github/mperham/sidekiq/Sidekiq%2FClient:push_bulk + namespace.all_projects.find_in_batches do |batch| + args = batch.map { |project| [:index, project.class.to_s, project.id, project.es_id] } + ElasticIndexerWorker.bulk_perform_async(args) + end + end + + def delete_from_index(namespace) + namespace.all_projects.find_in_batches do |batch| + args = batch.map { |project| [:delete, project.class.to_s, project.id, project.es_id] } + ElasticIndexerWorker.bulk_perform_async(args) + end + end +end diff --git a/ee/changelogs/unreleased/1607-elastic_feature_flag_per_group.yml b/ee/changelogs/unreleased/1607-elastic_feature_flag_per_group.yml new file mode 100644 index 00000000000000..3bf6880ed65e07 --- /dev/null +++ b/ee/changelogs/unreleased/1607-elastic_feature_flag_per_group.yml @@ -0,0 +1,5 @@ +--- +title: Allow per-project and per-group enabling of Elasticsearch indexing +merge_request: 9861 +author: +type: added diff --git a/ee/db/migrate/20190318020549_add_elasticsearch_limit_indexing_to_application_setting.rb b/ee/db/migrate/20190318020549_add_elasticsearch_limit_indexing_to_application_setting.rb new file mode 100644 index 00000000000000..673b9e38164efd --- /dev/null +++ b/ee/db/migrate/20190318020549_add_elasticsearch_limit_indexing_to_application_setting.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddElasticsearchLimitIndexingToApplicationSetting < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, :elasticsearch_limit_indexing, :boolean, default: false, allow_null: false + end + + def down + remove_column :application_settings, :elasticsearch_limit_indexing + end +end diff --git a/ee/db/migrate/20190318021429_add_elastic_namespace_link_and_elastic_project_link.rb b/ee/db/migrate/20190318021429_add_elastic_namespace_link_and_elastic_project_link.rb new file mode 100644 index 00000000000000..25fa57aeae3339 --- /dev/null +++ b/ee/db/migrate/20190318021429_add_elastic_namespace_link_and_elastic_project_link.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddElasticNamespaceLinkAndElasticProjectLink < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :elasticsearch_indexed_namespaces, id: false do |t| + t.timestamps_with_timezone null: false + + t.references :namespace, nil: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + end + + create_table :elasticsearch_indexed_projects, id: false do |t| + t.timestamps_with_timezone null: false + + t.references :project, nil: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + end + end +end diff --git a/ee/lib/ee/gitlab/fake_application_settings.rb b/ee/lib/ee/gitlab/fake_application_settings.rb new file mode 100644 index 00000000000000..7883d2020c7427 --- /dev/null +++ b/ee/lib/ee/gitlab/fake_application_settings.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module EE + module Gitlab + module FakeApplicationSettings + def elasticsearch_indexes_project?(_project) + false + end + + def elasticsearch_indexes_namespace?(_namespace) + false + end + end + end +end diff --git a/ee/lib/tasks/gitlab/elastic.rake b/ee/lib/tasks/gitlab/elastic.rake index ab9cefd2e9c417..cc221146d354e5 100644 --- a/ee/lib/tasks/gitlab/elastic.rake +++ b/ee/lib/tasks/gitlab/elastic.rake @@ -57,7 +57,7 @@ namespace :gitlab do projects = apply_project_filters(Project.with_wiki_enabled) projects.find_each do |project| - unless project.wiki.empty? + if project.use_elasticsearch? && !project.wiki.empty? puts "Indexing wiki of #{project.full_name}..." begin diff --git a/ee/spec/factories/elasticsearch_indexed_containers.rb b/ee/spec/factories/elasticsearch_indexed_containers.rb new file mode 100644 index 00000000000000..8ce0c0ed9169e6 --- /dev/null +++ b/ee/spec/factories/elasticsearch_indexed_containers.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :elasticsearch_indexed_namespace do + namespace + end + + factory :elasticsearch_indexed_project do + project + end +end diff --git a/ee/spec/features/admin/admin_settings_spec.rb b/ee/spec/features/admin/admin_settings_spec.rb index fac81177404bbd..82c3864f18f038 100644 --- a/ee/spec/features/admin/admin_settings_spec.rb +++ b/ee/spec/features/admin/admin_settings_spec.rb @@ -45,15 +45,64 @@ expect(page).to have_content "Application settings saved successfully" end - it 'Enable elastic search indexing' do - visit integrations_admin_application_settings_path - page.within('.as-elasticsearch') do - check 'Elasticsearch indexing' - click_button 'Save changes' + context 'Elasticsearch settings' do + before do + visit integrations_admin_application_settings_path end - expect(Gitlab::CurrentSettings.elasticsearch_indexing).to be_truthy - expect(page).to have_content "Application settings saved successfully" + it 'Enable elastic search indexing' do + page.within('.as-elasticsearch') do + check 'Elasticsearch indexing' + click_button 'Save changes' + end + + expect(Gitlab::CurrentSettings.elasticsearch_indexing).to be_truthy + expect(page).to have_content "Application settings saved successfully" + end + + it 'Allows limiting projects and namespaces to index', :js do + project = create(:project) + namespace = create(:namespace) + + page.within('.as-elasticsearch') do + expect(page).not_to have_content('Namespaces to index') + expect(page).not_to have_content('Projects to index') + + check 'Limit namespaces and projects that can be indexed' + + expect(page).to have_content('Namespaces to index') + expect(page).to have_content('Projects to index') + + fill_in 'Namespaces to index', with: namespace.name + wait_for_requests + end + + page.within('#select2-drop') do + expect(page).to have_content(namespace.full_path) + end + + page.within('.as-elasticsearch') do + find('.js-limit-namespaces .select2-choices input[type=text]').native.send_keys(:enter) + + fill_in 'Projects to index', with: project.name + wait_for_requests + end + + page.within('#select2-drop') do + expect(page).to have_content(project.full_name) + end + + page.within('.as-elasticsearch') do + find('.js-limit-projects .select2-choices input[type=text]').native.send_keys(:enter) + + click_button 'Save changes' + end + + expect(Gitlab::CurrentSettings.elasticsearch_limit_indexing).to be_truthy + expect(ElasticsearchIndexedNamespace.exists?(namespace_id: namespace.id)).to be_truthy + expect(ElasticsearchIndexedProject.exists?(project_id: project.id)).to be_truthy + expect(page).to have_content "Application settings saved successfully" + end end it 'Enable Slack application' do diff --git a/ee/spec/models/application_setting_spec.rb b/ee/spec/models/application_setting_spec.rb index 0a5392b9c37cce..6046d96911bf02 100644 --- a/ee/spec/models/application_setting_spec.rb +++ b/ee/spec/models/application_setting_spec.rb @@ -209,6 +209,79 @@ def expect_is_es_licensed aws_secret_access_key: 'test-secret-access-key' ) end + + context 'limiting namespaces and projects' do + before do + setting.update!(elasticsearch_indexing: true) + setting.update!(elasticsearch_limit_indexing: true) + end + + context 'namespaces' do + let(:namespaces) { create_list(:namespace, 3) } + + it 'creates ElasticsearchIndexedNamespace objects when given elasticsearch_namespace_ids' do + expect do + setting.update!(elasticsearch_namespace_ids: namespaces.map(&:id).join(',')) + end.to change { ElasticsearchIndexedNamespace.count }.by(3) + end + + it 'deletes ElasticsearchIndexedNamespace objects not in elasticsearch_namespace_ids' do + create :elasticsearch_indexed_namespace, namespace: namespaces.last + + expect do + setting.update!(elasticsearch_namespace_ids: namespaces.first(2).map(&:id).join(',')) + end.to change { ElasticsearchIndexedNamespace.count }.from(1).to(2) + + expect(ElasticsearchIndexedNamespace.where(namespace_id: namespaces.last.id)).not_to exist + end + + it 'tells you if a namespace is allowed to be indexed' do + create :elasticsearch_indexed_namespace, namespace: namespaces.last + + expect(setting.elasticsearch_indexes_namespace?(namespaces.last)).to be_truthy + expect(setting.elasticsearch_indexes_namespace?(namespaces.first)).to be_falsey + end + end + + context 'projects' do + let(:projects) { create_list(:project, 3) } + + it 'creates ElasticsearchIndexedProject objects when given elasticsearch_project_ids' do + expect do + setting.update!(elasticsearch_project_ids: projects.map(&:id).join(',')) + end.to change { ElasticsearchIndexedProject.count }.by(3) + end + + it 'deletes ElasticsearchIndexedProject objects not in elasticsearch_project_ids' do + create :elasticsearch_indexed_project, project: projects.last + + expect do + setting.update!(elasticsearch_project_ids: projects.first(2).map(&:id).join(',')) + end.to change { ElasticsearchIndexedProject.count }.from(1).to(2) + + expect(ElasticsearchIndexedProject.where(project_id: projects.last.id)).not_to exist + end + + it 'tells you if a project is allowed to be indexed' do + create :elasticsearch_indexed_project, project: projects.last + + expect(setting.elasticsearch_indexes_project?(projects.last)).to be_truthy + expect(setting.elasticsearch_indexes_project?(projects.first)).to be_falsey + end + end + + it 'returns projects that are allowed to be indexed' do + project1 = create(:project) + projects = create_list(:project, 3) + + setting.update!( + elasticsearch_project_ids: projects.map(&:id).join(','), + elasticsearch_namespace_ids: project1.namespace.id.to_s + ) + + expect(setting.elasticsearch_limited_projects).to match_array(projects << project1) + end + end end describe 'custom project templates' do diff --git a/ee/spec/models/concerns/elastic/issue_spec.rb b/ee/spec/models/concerns/elastic/issue_spec.rb index d468d235ac8db9..33d8d99232dbfa 100644 --- a/ee/spec/models/concerns/elastic/issue_spec.rb +++ b/ee/spec/models/concerns/elastic/issue_spec.rb @@ -7,6 +7,52 @@ let(:project) { create :project } + context 'when limited indexing is on' do + set(:project) { create :project, name: 'test1' } + set(:issue) { create :issue, project: project} + + before do + stub_ee_application_setting(elasticsearch_limit_indexing: true) + end + + context 'when the project is not enabled specifically' do + context '#searchable?' do + it 'returns false' do + expect(issue.searchable?).to be_falsey + end + end + end + + context 'when a project is enabled specifically' do + before do + create :elasticsearch_indexed_project, project: project + end + + context '#searchable?' do + it 'returns true' do + expect(issue.searchable?).to be_truthy + end + end + end + + context 'when a group is enabled' do + set(:group) { create(:group) } + + before do + create :elasticsearch_indexed_namespace, namespace: group + end + + context '#searchable?' do + it 'returns true' do + project = create :project, name: 'test1', group: group + issue = create :issue, project: project + + expect(issue.searchable?).to be_truthy + end + end + end + end + it "searches issues" do Sidekiq::Testing.inline! do create :issue, title: 'bla-bla term1', project: project diff --git a/ee/spec/models/concerns/elastic/merge_request_spec.rb b/ee/spec/models/concerns/elastic/merge_request_spec.rb index b8d883fb7ec72d..91ccf0c2f3332f 100644 --- a/ee/spec/models/concerns/elastic/merge_request_spec.rb +++ b/ee/spec/models/concerns/elastic/merge_request_spec.rb @@ -5,6 +5,15 @@ stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) end + it_behaves_like 'limited indexing is enabled' do + set(:object) { create :merge_request, source_project: project } + set(:group) { create(:group) } + let(:group_object) do + project = create :project, name: 'test1', group: group + create :merge_request, source_project: project + end + end + it "searches merge requests" do project = create :project, :repository diff --git a/ee/spec/models/concerns/elastic/milestone_spec.rb b/ee/spec/models/concerns/elastic/milestone_spec.rb index 47a5b13b6a7df1..4e8873995c0529 100644 --- a/ee/spec/models/concerns/elastic/milestone_spec.rb +++ b/ee/spec/models/concerns/elastic/milestone_spec.rb @@ -5,6 +5,15 @@ stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) end + it_behaves_like 'limited indexing is enabled' do + set(:object) { create :milestone, project: project } + set(:group) { create(:group) } + let(:group_object) do + project = create :project, name: 'test1', group: group + create :milestone, project: project + end + end + it "searches milestones" do project = create :project diff --git a/ee/spec/models/concerns/elastic/note_spec.rb b/ee/spec/models/concerns/elastic/note_spec.rb index 4d1d6a6024748d..3ef21b334df260 100644 --- a/ee/spec/models/concerns/elastic/note_spec.rb +++ b/ee/spec/models/concerns/elastic/note_spec.rb @@ -5,6 +5,35 @@ stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) end + it_behaves_like 'limited indexing is enabled' do + set(:object) { create :note, project: project } + set(:group) { create(:group) } + let(:group_object) do + project = create :project, name: 'test1', group: group + create :note, project: project + end + + context '#searchable?' do + before do + create :elasticsearch_indexed_project, project: project + end + + it 'also works on diff notes' do + notes = [] + notes << create(:diff_note_on_merge_request, note: "term") + notes << create(:diff_note_on_commit, note: "term") + notes << create(:legacy_diff_note_on_merge_request, note: "term") + notes << create(:legacy_diff_note_on_commit, note: "term") + + notes.each do |note| + create :elasticsearch_indexed_project, project: note.noteable.project + + expect(note.searchable?).to be_truthy + end + end + end + end + it "searches notes" do issue = create :issue diff --git a/ee/spec/models/concerns/elastic/project_spec.rb b/ee/spec/models/concerns/elastic/project_spec.rb index a93892a83289e6..93f38794883975 100644 --- a/ee/spec/models/concerns/elastic/project_spec.rb +++ b/ee/spec/models/concerns/elastic/project_spec.rb @@ -5,6 +5,93 @@ stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) end + context 'when limited indexing is on' do + set(:project) { create :project, name: 'test1' } + + before do + stub_ee_application_setting(elasticsearch_limit_indexing: true) + end + + context 'when the project is not enabled specifically' do + context '#searchable?' do + it 'returns false' do + expect(project.searchable?).to be_falsey + end + end + + context '#use_elasticsearch?' do + it 'returns false' do + expect(project.use_elasticsearch?).to be_falsey + end + end + end + + context 'when a project is enabled specifically' do + before do + create :elasticsearch_indexed_project, project: project + end + + context '#searchable?' do + it 'returns true' do + expect(project.searchable?).to be_truthy + end + end + + context '#use_elasticsearch?' do + it 'returns true' do + expect(project.use_elasticsearch?).to be_truthy + end + end + + it 'only indexes enabled projects' do + Sidekiq::Testing.inline! do + # We have to trigger indexing of the previously-created project because we don't have a way to + # enable ES for it before it's created, at which point it won't be indexed anymore + ElasticIndexerWorker.perform_async(:index, project.class.to_s, project.id, project.es_id) + create :project, path: 'test2', description: 'awesome project' + create :project + + Gitlab::Elastic::Helper.refresh_index + end + + expect(described_class.elastic_search('test*', options: { project_ids: :any }).total_count).to eq(1) + expect(described_class.elastic_search('test2', options: { project_ids: :any }).total_count).to eq(0) + end + end + + context 'when a group is enabled' do + set(:group) { create(:group) } + + before do + create :elasticsearch_indexed_namespace, namespace: group + end + + context '#searchable?' do + it 'returns true' do + project = create :project, name: 'test1', group: group + + expect(project.searchable?).to be_truthy + end + end + + it 'indexes only projects under the group', :nested_groups do + Sidekiq::Testing.inline! do + create :project, name: 'test1', group: create(:group, parent: group) + create :project, name: 'test2', description: 'awesome project' + create :project, name: 'test3', group: group + create :project, path: 'someone_elses_project', name: 'test4' + + Gitlab::Elastic::Helper.refresh_index + end + + expect(described_class.elastic_search('test*', options: { project_ids: :any }).total_count).to eq(2) + expect(described_class.elastic_search('test3', options: { project_ids: :any }).total_count).to eq(1) + expect(described_class.elastic_search('test2', options: { project_ids: :any }).total_count).to eq(0) + expect(described_class.elastic_search('test4', options: { project_ids: :any }).total_count).to eq(0) + end + end + end + it "finds projects" do project_ids = [] diff --git a/ee/spec/models/concerns/elastic/snippet_spec.rb b/ee/spec/models/concerns/elastic/snippet_spec.rb index bd017574a9bfc6..f5fc42756150bc 100644 --- a/ee/spec/models/concerns/elastic/snippet_spec.rb +++ b/ee/spec/models/concerns/elastic/snippet_spec.rb @@ -5,6 +5,16 @@ stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) end + it 'always returns global result for Elasticsearch indexing for #use_elasticsearch?' do + snippet = create :snippet + + expect(snippet.use_elasticsearch?).to eq(true) + + stub_ee_application_setting(elasticsearch_indexing: false) + + expect(snippet.use_elasticsearch?).to eq(false) + end + context 'searching snippets by code' do let!(:author) { create(:user) } let!(:project) { create(:project) } diff --git a/ee/spec/models/ee/namespace_spec.rb b/ee/spec/models/ee/namespace_spec.rb index 8fa33c3a6d7da3..8d51832ec0420b 100644 --- a/ee/spec/models/ee/namespace_spec.rb +++ b/ee/spec/models/ee/namespace_spec.rb @@ -30,4 +30,30 @@ it_behaves_like 'plan helper', namespace_plan end end + + describe '#use_elasticsearch?' do + let(:namespace) { create :namespace } + + it 'returns false if elasticsearch indexing is disabled' do + stub_ee_application_setting(elasticsearch_indexing: false) + + expect(namespace.use_elasticsearch?).to eq(false) + end + + it 'returns true if elasticsearch indexing enabled but limited indexing disabled' do + stub_ee_application_setting(elasticsearch_indexing: true, elasticsearch_limit_indexing: false) + + expect(namespace.use_elasticsearch?).to eq(true) + end + + it 'returns true if it is enabled specifically' do + stub_ee_application_setting(elasticsearch_indexing: true, elasticsearch_limit_indexing: true) + + expect(namespace.use_elasticsearch?).to eq(false) + + ::Gitlab::CurrentSettings.update!(elasticsearch_namespace_ids: namespace.id.to_s) + + expect(namespace.use_elasticsearch?).to eq(true) + end + end end diff --git a/ee/spec/models/elasticsearch_indexed_namespace_spec.rb b/ee/spec/models/elasticsearch_indexed_namespace_spec.rb new file mode 100644 index 00000000000000..fa9e4a1ee6cfb9 --- /dev/null +++ b/ee/spec/models/elasticsearch_indexed_namespace_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ElasticsearchIndexedNamespace do + before do + stub_ee_application_setting(elasticsearch_indexing: true) + end + + it_behaves_like 'an elasticsearch indexed container' do + let(:container) { :elasticsearch_indexed_namespace } + let(:attribute) { :namespace_id } + let(:index_action) do + expect(ElasticNamespaceIndexerWorker).to receive(:perform_async).with(subject.namespace_id, :index) + end + let(:delete_action) do + expect(ElasticNamespaceIndexerWorker).to receive(:perform_async).with(subject.namespace_id, :delete) + end + end +end diff --git a/ee/spec/models/elasticsearch_indexed_project_spec.rb b/ee/spec/models/elasticsearch_indexed_project_spec.rb new file mode 100644 index 00000000000000..313e0d7af6995e --- /dev/null +++ b/ee/spec/models/elasticsearch_indexed_project_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ElasticsearchIndexedProject do + before do + stub_ee_application_setting(elasticsearch_indexing: true) + end + + it_behaves_like 'an elasticsearch indexed container' do + let(:container) { :elasticsearch_indexed_project } + let(:attribute) { :project_id } + let(:index_action) do + expect(ElasticIndexerWorker).to receive(:perform_async).with(:index, 'Project', subject.project_id, any_args) + end + let(:delete_action) do + expect(ElasticIndexerWorker).to receive(:perform_async).with(:delete, 'Project', subject.project_id, any_args) + end + end +end diff --git a/ee/spec/models/project_import_state_spec.rb b/ee/spec/models/project_import_state_spec.rb index 0e29c7109e3a80..3ea5540cb18571 100644 --- a/ee/spec/models/project_import_state_spec.rb +++ b/ee/spec/models/project_import_state_spec.rb @@ -52,16 +52,18 @@ context 'no index status' do it 'schedules a full index of the repository' do - expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(import_state.project_id, nil) + expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(import_state.project_id, Gitlab::Git::BLANK_SHA) import_state.finish end end context 'with index status' do - let!(:index_status) { import_state.project.create_index_status!(indexed_at: Time.now, last_commit: 'foo') } + let!(:index_status) { import_state.project.index_status } it 'schedules a progressive index of the repository' do + index_status.update!(indexed_at: Time.now, last_commit: 'foo') + expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(import_state.project_id, index_status.last_commit) import_state.finish diff --git a/ee/spec/services/ee/git/branch_push_service_spec.rb b/ee/spec/services/ee/git/branch_push_service_spec.rb index 6d1627b67e8956..db22de2ad3fbbe 100644 --- a/ee/spec/services/ee/git/branch_push_service_spec.rb +++ b/ee/spec/services/ee/git/branch_push_service_spec.rb @@ -65,6 +65,47 @@ execute_service(project, user, oldrev, newrev, ref) end + + context 'when limited indexing is on' do + before do + stub_ee_application_setting(elasticsearch_limit_indexing: true) + end + + context 'when the project is not enabled specifically' do + it 'does not run ElasticCommitIndexerWorker' do + expect(ElasticCommitIndexerWorker).not_to receive(:perform_async) + + subject.execute + end + end + + context 'when a project is enabled specifically' do + before do + create :elasticsearch_indexed_project, project: project + end + + it 'runs ElasticCommitIndexerWorker' do + expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id, oldrev, newrev) + + subject.execute + end + end + + context 'when a group is enabled' do + let(:group) { create(:group) } + let(:project) { create(:project, :repository, :mirror, group: group) } + + before do + create :elasticsearch_indexed_namespace, namespace: group + end + + it 'runs ElasticCommitIndexerWorker' do + expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id, oldrev, newrev) + + subject.execute + end + end + end end end diff --git a/ee/spec/support/shared_examples/models/concerns/elastic/limited_indexing.rb b/ee/spec/support/shared_examples/models/concerns/elastic/limited_indexing.rb new file mode 100644 index 00000000000000..543f0642c56bce --- /dev/null +++ b/ee/spec/support/shared_examples/models/concerns/elastic/limited_indexing.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +shared_examples 'limited indexing is enabled' do + set(:project) { create :project, :repository, name: 'test1' } + + before do + stub_ee_application_setting(elasticsearch_limit_indexing: true) + end + + context 'when the project is not enabled specifically' do + context '#searchable?' do + it 'returns false' do + expect(object.searchable?).to be_falsey + end + end + end + + context 'when a project is enabled specifically' do + before do + create :elasticsearch_indexed_project, project: project + end + + context '#searchable?' do + it 'returns true' do + expect(object.searchable?).to be_truthy + end + end + end + + context 'when a group is enabled' do + before do + create :elasticsearch_indexed_namespace, namespace: group + end + + context '#searchable?' do + it 'returns true' do + expect(group_object.searchable?).to be_truthy + end + end + end +end diff --git a/ee/spec/support/shared_examples/models/elasticsearch_indexed_container.rb b/ee/spec/support/shared_examples/models/elasticsearch_indexed_container.rb new file mode 100644 index 00000000000000..83728ea9f38e16 --- /dev/null +++ b/ee/spec/support/shared_examples/models/elasticsearch_indexed_container.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +shared_examples 'an elasticsearch indexed container' do + describe 'validations' do + subject { create container } + + it 'validates uniqueness of main attribute' do + is_expected.to validate_uniqueness_of(attribute) + end + end + + describe 'callbacks' do + subject { build container } + + describe 'on save' do + it 'triggers index_project' do + is_expected.to receive(:index) + + subject.save! + end + + it 'performs the expected action' do + index_action + + subject.save! + end + end + + describe 'on destroy' do + subject { create container } + + it 'triggers delete_from_index' do + is_expected.to receive(:delete_from_index) + + subject.destroy! + end + + it 'performs the expected action' do + delete_action + + subject.destroy! + end + end + end +end diff --git a/ee/spec/workers/elastic_batch_project_indexer_worker_spec.rb b/ee/spec/workers/elastic_batch_project_indexer_worker_spec.rb index 3d487172fa3e98..878269f3f592b3 100644 --- a/ee/spec/workers/elastic_batch_project_indexer_worker_spec.rb +++ b/ee/spec/workers/elastic_batch_project_indexer_worker_spec.rb @@ -5,6 +5,26 @@ let(:projects) { create_list(:project, 2) } describe '#perform' do + before do + stub_ee_application_setting(elasticsearch_indexing: true) + end + + context 'with elasticsearch only enabled for a particular project' do + before do + stub_ee_application_setting(elasticsearch_limit_indexing: true) + create :elasticsearch_indexed_project, project: projects.first + end + + it 'only indexes the enabled project' do + projects.each { |project| expect_index(project, false).and_call_original } + + expect(Gitlab::Elastic::Indexer).to receive(:new).with(projects.first).and_return(double(run: true)) + expect(Gitlab::Elastic::Indexer).not_to receive(:new).with(projects.last) + + worker.perform(projects.first.id, projects.last.id) + end + end + it 'runs the indexer for projects in the batch range' do projects.each { |project| expect_index(project, false) } @@ -32,7 +52,7 @@ context 'update_index = false' do it 'indexes all projects it receives even if already indexed' do - projects.first.build_index_status.update!(last_commit: 'foo') + projects.first.index_status.update!(last_commit: 'foo') expect_index(projects.first, false).and_call_original expect_next_instance_of(Gitlab::Elastic::Indexer) do |indexer| @@ -45,8 +65,6 @@ context 'with update_index' do it 'reindexes projects that were already indexed' do - projects.first.create_index_status! - expect_index(projects.first, true) expect_index(projects.last, true) @@ -54,7 +72,7 @@ end it 'starts indexing at the last indexed commit' do - projects.first.create_index_status!(last_commit: 'foo') + projects.first.index_status.update!(last_commit: 'foo') expect_index(projects.first, true).and_call_original expect_any_instance_of(Gitlab::Elastic::Indexer).to receive(:run).with('foo') diff --git a/ee/spec/workers/elastic_indexer_worker_spec.rb b/ee/spec/workers/elastic_indexer_worker_spec.rb index 59ceaa6adac7b5..c168956b4da067 100644 --- a/ee/spec/workers/elastic_indexer_worker_spec.rb +++ b/ee/spec/workers/elastic_indexer_worker_spec.rb @@ -106,4 +106,31 @@ expect(Elasticsearch::Model.search('*').total_count).to be(0) end + + it 'indexes all nested objects for a Project' do + # To be able to access it outside the following block + project = nil + + Sidekiq::Testing.disable! do + project = create :project, :repository + create :issue, project: project + create :milestone, project: project + create :note, project: project + create :merge_request, target_project: project, source_project: project + create :project_snippet, project: project + end + + expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id).and_call_original + + # Nothing should be in the index at this point + expect(Elasticsearch::Model.search('*').total_count).to be(0) + + Sidekiq::Testing.inline! do + subject.perform("index", "Project", project.id, project.es_id) + end + Gitlab::Elastic::Helper.refresh_index + + ## All database objects + data from repository. The absolute value does not matter + expect(Elasticsearch::Model.search('*').total_count).to be > 40 + end end diff --git a/ee/spec/workers/elastic_namespace_indexer_worker_spec.rb b/ee/spec/workers/elastic_namespace_indexer_worker_spec.rb new file mode 100644 index 00000000000000..8bc024c594d11b --- /dev/null +++ b/ee/spec/workers/elastic_namespace_indexer_worker_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ElasticNamespaceIndexerWorker, :elastic do + subject { described_class.new } + + before do + stub_ee_application_setting(elasticsearch_indexing: true) + stub_ee_application_setting(elasticsearch_limit_indexing: true) + end + + it 'returns true if ES disabled' do + stub_ee_application_setting(elasticsearch_indexing: false) + + expect(ElasticIndexerWorker).not_to receive(:perform_async) + + expect(subject.perform(1, "index")).to be_truthy + end + + it 'returns true if limited indexing is not enabled' do + stub_ee_application_setting(elasticsearch_limit_indexing: false) + + expect(ElasticIndexerWorker).not_to receive(:perform_async) + + expect(subject.perform(1, "index")).to be_truthy + end + + describe 'indexing and deleting' do + set(:namespace) { create :namespace } + let(:projects) { create_list :project, 3, namespace: namespace } + + it 'indexes all projects belonging to the namespace' do + args = projects.map { |project| [:index, project.class.to_s, project.id, project.es_id] } + expect(ElasticIndexerWorker).to receive(:bulk_perform_async).with(args) + + subject.perform(namespace.id, :index) + end + + it 'deletes all projects belonging to the namespace' do + args = projects.map { |project| [:delete, project.class.to_s, project.id, project.es_id] } + expect(ElasticIndexerWorker).to receive(:bulk_perform_async).with(args) + + subject.perform(namespace.id, :delete) + end + end +end diff --git a/ee/spec/workers/post_receive_spec.rb b/ee/spec/workers/post_receive_spec.rb index e3dee2dbadf6cc..7daf6fd9eb8556 100644 --- a/ee/spec/workers/post_receive_spec.rb +++ b/ee/spec/workers/post_receive_spec.rb @@ -70,5 +70,55 @@ described_class.new.perform(gl_repository, key_id, base64_changes) end + + context 'when limited indexing is on' do + before do + stub_ee_application_setting( + elasticsearch_search: true, + elasticsearch_indexing: true, + elasticsearch_limit_indexing: true + ) + end + + context 'when the project is not enabled specifically' do + it 'does not trigger wiki index update' do + expect(ProjectWiki).not_to receive(:new) + + described_class.new.perform(gl_repository, key_id, base64_changes) + end + end + + context 'when a project is enabled specifically' do + before do + create :elasticsearch_indexed_project, project: project + end + + it 'triggers wiki index update' do + expect_next_instance_of(ProjectWiki) do |project_wiki| + expect(project_wiki).to receive(:index_blobs) + end + + described_class.new.perform(gl_repository, key_id, base64_changes) + end + end + + context 'when a group is enabled' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:key) { create(:key, user: group.owner) } + + before do + create :elasticsearch_indexed_namespace, namespace: group + end + + it 'triggers wiki index update' do + expect_next_instance_of(ProjectWiki) do |project_wiki| + expect(project_wiki).to receive(:index_blobs) + end + + described_class.new.perform(gl_repository, key_id, base64_changes) + end + end + end end end diff --git a/lib/gitlab/fake_application_settings.rb b/lib/gitlab/fake_application_settings.rb index 77f7d9490f35f1..014dab83ca2fb6 100644 --- a/lib/gitlab/fake_application_settings.rb +++ b/lib/gitlab/fake_application_settings.rb @@ -32,3 +32,5 @@ def initialize(options = {}) alias_method :has_attribute?, :[] end end + +Gitlab::FakeApplicationSettings.prepend(EE::Gitlab::FakeApplicationSettings) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c15bf573ea3a6c..a989c212ebb9ba 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -749,6 +749,9 @@ msgstr "" msgid "Advanced permissions, Large File Storage and Two-Factor authentication settings." msgstr "" +msgid "Advanced search functionality" +msgstr "" + msgid "Advanced settings" msgstr "" @@ -3742,6 +3745,12 @@ msgstr "" msgid "Elasticsearch integration. Elasticsearch AWS IAM." msgstr "" +msgid "Elastic|Select groups to enable." +msgstr "" + +msgid "Elastic|Select projects to enable." +msgstr "" + msgid "Email" msgstr "" @@ -6327,6 +6336,9 @@ msgstr "" msgid "Licenses" msgstr "" +msgid "Limit namespaces and projects that can be indexed" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "" @@ -6908,6 +6920,9 @@ msgstr "" msgid "Name:" msgstr "" +msgid "Namespaces to index" +msgstr "" + msgid "Naming, tags, avatar" msgstr "" @@ -8307,6 +8322,9 @@ msgstr "" msgid "Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group." msgstr "" +msgid "Projects to index" +msgstr "" + msgid "Projects with write access" msgstr "" -- GitLab From e9fdc4daa7a4118260ea727afa3f636f6ed1cde5 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 20 Mar 2019 15:37:28 +0530 Subject: [PATCH 2/5] Clean up to reduce duplication Cleans up jQuery and Select2 config to prevent duplication while initializing dropdowns. --- .../integrations/index.js | 132 ++++++++---------- locale/gitlab.pot | 4 +- 2 files changed, 61 insertions(+), 75 deletions(-) diff --git a/ee/app/assets/javascripts/pages/admin/application_settings/integrations/index.js b/ee/app/assets/javascripts/pages/admin/application_settings/integrations/index.js index 3b736a52d89c38..e37a0e9912f030 100644 --- a/ee/app/assets/javascripts/pages/admin/application_settings/integrations/index.js +++ b/ee/app/assets/javascripts/pages/admin/application_settings/integrations/index.js @@ -3,86 +3,72 @@ import { s__ } from '~/locale'; import '~/flash'; import Api from '~/api'; -const onLimitCheckboxChange = function onLimitCheckboxChange( - e, - $limitByNamespaces, - $limitByProjects, -) { - const $namespacesSelect = $('.select2', $limitByNamespaces); - const $projectsSelect = $('.select2', $limitByNamespaces); - - $namespacesSelect.select2('data', null); - $projectsSelect.select2('data', null); - $limitByNamespaces.toggleClass('hidden', !e.currentTarget.checked); - $limitByProjects.toggleClass('hidden', !e.currentTarget.checked); +const onLimitCheckboxChange = (checked, $limitByNamespaces, $limitByProjects) => { + $limitByNamespaces.find('.select2').select2('data', null); + $limitByNamespaces.find('.select2').select2('data', null); + $limitByNamespaces.toggleClass('hidden', !checked); + $limitByProjects.toggleClass('hidden', !checked); }; -export default function elasticsearchForm() { +const getDropdownConfig = (placeholder, apiPath, textProp) => ({ + placeholder, + multiple: true, + initSelection($el, callback) { + callback($el.data('selected')); + }, + ajax: { + url: Api.buildUrl(apiPath), + dataType: 'JSON', + quietMillis: 250, + data(search) { + return { + search, + }; + }, + results(data) { + return { + results: data.map(entity => ({ + id: entity.id, + text: entity[textProp], + })), + }; + }, + }, +}); + +document.addEventListener('DOMContentLoaded', () => { const $container = $('#js-elasticsearch-settings'); - const $limitCheckbox = $('.js-limit-checkbox', $container); - const $limitByNamespaces = $('.js-limit-namespaces', $container); - const $limitByProjects = $('.js-limit-projects', $container); - const $namespacesDropdown = $('.js-elasticsearch-namespaces', $container); - const $projectsDropdown = $('.js-elasticsearch-projects', $container); - $limitCheckbox.on('change', e => onLimitCheckboxChange(e, $limitByNamespaces, $limitByProjects)); + $container + .find('.js-limit-checkbox') + .on('change', e => + onLimitCheckboxChange( + e.currentTarget.checked, + $container.find('.js-limit-namespaces'), + $container.find('.js-limit-projects'), + ), + ); import(/* webpackChunkName: 'select2' */ 'select2/select2') .then(() => { - $namespacesDropdown.select2({ - placeholder: s__('Elastic|Select groups to enable.'), - multiple: true, - initSelection($el, callback) { - callback($el.data('selected')); - }, - ajax: { - url: Api.buildUrl(Api.groupsPath), - dataType: 'JSON', - quietMillis: 250, - data(search) { - return { - search, - }; - }, - results(data) { - return { - results: data.map(group => ({ - id: group.id, - text: group.full_name, - })), - }; - }, - }, - }); - $projectsDropdown.select2({ - placeholder: s__('Elastic|Select projects to enable.'), - multiple: true, - initSelection($el, callback) { - callback($el.data('selected')); - }, - ajax: { - url: Api.buildUrl(Api.projectsPath), - dataType: 'JSON', - quietMillis: 250, - data(search) { - return { - search, - }; - }, - results(data) { - return { - results: data.map(project => ({ - id: project.id, - text: project.full_name, - })), - }; - }, - }, - }); + $container + .find('.js-elasticsearch-namespaces') + .select2( + getDropdownConfig( + s__('Elastic|None. Select namespaces to index.'), + Api.namespacesPath, + 'full_path', + ), + ); + $container + .find('.js-elasticsearch-projects') + .select2( + getDropdownConfig( + s__('Elastic|None. Select projects to index.'), + Api.projectsPath, + 'name_with_namespace', + ), + ); }) .catch(() => {}); -} - -document.addEventListener('DOMContentLoaded', () => { - elasticsearchForm(); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a989c212ebb9ba..794e3b7f8ca566 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3745,10 +3745,10 @@ msgstr "" msgid "Elasticsearch integration. Elasticsearch AWS IAM." msgstr "" -msgid "Elastic|Select groups to enable." +msgid "Elastic|None. Select namespaces to index." msgstr "" -msgid "Elastic|Select projects to enable." +msgid "Elastic|None. Select projects to index." msgstr "" msgid "Email" -- GitLab From f13252e0300351a9cdf5a60d02492569be759c64 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Wed, 20 Mar 2019 15:39:36 +0530 Subject: [PATCH 3/5] Add missing select2 icons --- app/assets/images/select2.png | Bin 0 -> 613 bytes app/assets/images/select2x2.png | Bin 0 -> 845 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100755 app/assets/images/select2.png create mode 100755 app/assets/images/select2x2.png diff --git a/app/assets/images/select2.png b/app/assets/images/select2.png new file mode 100755 index 0000000000000000000000000000000000000000..1d804ffb99699b9e030f1010314de0970b5a000d GIT binary patch literal 613 zcmeAS@N?(olHy`uVBq!ia0y~yV6b6eV9?-TV_;xdw0q`%1_mY_PZ!6Kid%1Qp7$0> zlsW!!D~DH4>ovWkSS^i{9ejdiD^srVF0v8%yFfmG! z5v+V0bYuO}?kTN}9wi1oGwk-ZzJF-BLnl}B_VvmKr891}*50!|e}-?frsrudp*=dy z|BnZ1h`f6ou|f8sT-Xk7wO2wd{uzhdL=<_ddGd}`b4;Gmpz~|Nb=BDZ>k}m1le-?8 zR&d)nJTu<5n@4R8>*Rw*a@^Ct&s61q=6v~tPSOWg4&fUOr3Wo-L(FEF&po0!hhvWG zu0x!>k3TBOKjpq5n3I33GWt?#^Ll5Fy@zLUscqukui&zBsr=LC+(pV0W*$Drwf|?} zi9JVGW%1r?k>p?&Jyzzx!5fmg!FPxJ{r`{d>}|8&A^YO@b$83zXRjr3EN`xDH|#hw zpJn~=eIZl%7g;GB$#CXQQ?M!dZtWAoJkN2)QGSl|7vFsH)9zX%^(f9^S;y79eBI95 z{rVGTx}8fkNOb>T9C56_>FM3KhCeh~rmr~c)#LQyJEXi;r5h zd`7M{tv)@{a`o-sA28la;0eDN{>pH}^LqYj#bV*D>zZ|UNf!%5-DQvdB7Ee=wtx@$ ZHP3713OV-QWME)m@O1TaS?83{1OOp#7GD4W literal 0 HcmV?d00001 diff --git a/app/assets/images/select2x2.png b/app/assets/images/select2x2.png new file mode 100755 index 0000000000000000000000000000000000000000..4bdd5c961d452c49dfa0789c2c7ffb82c238fc24 GIT binary patch literal 845 zcmeAS@N?(olHy`uVBq!ia0y~yV5neVU&r;B4q#jUq-sKR7Hmh!4%nX(7 zJxhB{qVA@>Z?VXGQ+`Q%Qmp2uy>H%}dGoaB(VS-HXD{*@nivl@BxIaCYdZVvDO>f4 z-sc@R$ZoNC&Z%mC&T)g_mK_~WUdXWA7G9&dHHJl6Rk3jCel?fd{+nXBCh1f+?e@^q zoZKTAx4q@*RGwh=%}uiAQ*9+qENZlpc#%@7bgg0jyG)5i;cNWjQm2Pob>0+vG3P)c1 zz2|0zJL`1bM}7z?5M0J#R^?opG*!X9fcuLmcW3RpN~?`t8ypJE4{mJ9zOwj%!bYzk zg&ozAM*PC7|FM+o-`2T$@bSkTB8y5l%=_%m^19{p_~xL481I#X z4(i-i-xqOSWSpbMn17Rzg&7>64j(QSe=%n`*pT30pdcZ@!y)(Kx3f{AQb?0<*3nD- zsagNzSfnde6EA*n|6JN2T*t}U#Jk|hJnmA3uTeWb#h7>4xqsMjQ9FG7jr)hnA1?h6 zQK7O&DD05l4_6HVn}k2}b_X8XcBuN{mc;rxjhauzS?AqO2-w6N6jsev-{ZaK%A39q zjypoME1MoXUHD4*p#$3-@ds7djyzjdF?;}rM$AawAs-sFNO6 z4N$sqtuS#d$I*95lLT{~AE>{nu;Wwm%z$Gm%0E8*UmM$*@VV*oB*C26f~ca_gv(%d sOo4{v!Ec^lS8A(PbR{@^F#N!7sHTw{zHH-d1_lNOPgg&ebxsLQ0RItrLI3~& literal 0 HcmV?d00001 -- GitLab From 03178609533bafb15a2c7b223f4ded507e0aa682 Mon Sep 17 00:00:00 2001 From: Mario de la Ossa Date: Thu, 21 Mar 2019 22:52:39 -0600 Subject: [PATCH 4/5] Documentation for limit ES indexing feature --- doc/integration/elasticsearch.md | 21 ++++++++++++++++++ .../img/limit_namespace_filter.png | Bin 0 -> 12777 bytes .../img/limit_namespaces_projects_options.png | Bin 0 -> 22650 bytes 3 files changed, 21 insertions(+) create mode 100644 doc/integration/img/limit_namespace_filter.png create mode 100644 doc/integration/img/limit_namespaces_projects_options.png diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index bc4740b2ee4ae1..7e36c422d45174 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -118,11 +118,32 @@ The following Elasticsearch settings are available: | `Use the new repository indexer (beta)` | Perform repository indexing using [GitLab Elasticsearch Indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). | | `Search with Elasticsearch enabled` | Enables/disables using Elasticsearch in search. | | `URL` | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://host1, https://host2:9200"). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://:@:9200/`). | +| `Limit namespaces and projects that can be indexed` | Enabling this will allow you to select namespaces and projects to index. All other namespaces and projects will use database search instead. Please note that if you enable this option but do not select any namespaces or projects, none will be indexed. [Read more below](#limiting-namespaces-and-projects). | `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) or [AWS EC2 Instance Profile Credentials](http://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli). The policies must be configured to allow `es:*` actions. | | `AWS Region` | The AWS region your Elasticsearch service is located in. | | `AWS Access Key` | The AWS access key. | | `AWS Secret Access Key` | The AWS secret access key. | +### Limiting namespaces and projects + +If you select `Limit namespaces and projects that can be indexed`, more options will become available +![limit namespaces and projects options](img/limit_namespaces_projects_options.png) + +You can select namespaces and projects to index exclusively. Please note that if the namespace is a group it will include +any sub-groups and projects belonging to those sub-groups to be indexed as well. + +You can filter the selection dropdown by writing part of the namespace or project name you're interested in. +![limit namespace filter](img/limit_namespace_filter.png) + +NOTE: **Note**: +If no namespaces or projects are selected, no Elasticsearch indexing will take place. + +CAUTION: **Warning**: +If you have already indexed your instance, you will have to regenerate the index in order to delete all existing data +for filtering to work correctly. To do this run the rake tasks `gitlab:elastic:create_empty_index` and +`gitlab:elastic:clear_index_status` Afterwards, removing a namespace or a projeect from the list will delete the data +from the Elasticsearch index as expected. + ## Disabling Elasticsearch To disable the Elasticsearch integration: diff --git a/doc/integration/img/limit_namespace_filter.png b/doc/integration/img/limit_namespace_filter.png new file mode 100644 index 0000000000000000000000000000000000000000..88f5caa41dbd2b23556aa329a0f8ec178e7ce365 GIT binary patch literal 12777 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 Date: Thu, 28 Mar 2019 15:52:43 +0530 Subject: [PATCH 5/5] Remove `form-control`, use simple import --- app/assets/images/select2-spinner.gif | Bin 0 -> 1849 bytes .../integrations/index.js | 43 ++++++++---------- .../_elasticsearch_form.html.haml | 4 +- 3 files changed, 22 insertions(+), 25 deletions(-) create mode 100755 app/assets/images/select2-spinner.gif diff --git a/app/assets/images/select2-spinner.gif b/app/assets/images/select2-spinner.gif new file mode 100755 index 0000000000000000000000000000000000000000..5b33f7e54f4e55b6b8774d86d96895db9af044b4 GIT binary patch literal 1849 zcmZ?wbhEHb6krfw_`<;O|Nnmm28ItGK6G_;J$UfI&CRWxnOR0g1_LQT@jthpYe=xOV}PrXo&hr>0|SHNKPl&;)Wnk16ovB4 zk_?5!ti+0({KOQ!%)GRG2F0H&oLmeH3_2iNK#pW!Emu%rV&YVMx`N5YiQ~BAgw-B0 zoexeNS|haiVB!YmWiCyJ1fDr4#BCFCd>Qd$LckJ-k2{n?o0~QVDs!HD=+qcvJ^w8C zVZ&8IJQA$==dFxXq{^5bSh!Sbbc|W}xw&{*n&Oq2IM^6?Q&<{IS=fxHD{&W_a&xCD zVslFw$Ss);sg`{%jvOZ)1$s7h9r<{GPf&lgWI{oPibSt-qN;-eYtQkQ1s*FoIOYm$ zsMzbq)Oe^gxKl)+nXy8-m6ex;<&fU~yDe>zB58esEIv%}O3bWW1zg5X&EbtK0xaAe zu{AzyyfJ2zs%xrdSS}2|U{s{DT6{raz!8yl zrUnKPiL5P$5+;%cnT z#3i87#N?#J%f_A7Tiv1M6dteNS(-rGjXJ)p* z3~RXLfWjqH0u(Mv9LGP(oz@a+m*(c#yN`ic_nL))2a8bC#s*tI>xDcQP6>vzxW2g{ zuvcuI=(|Eu0jU>D-fm2t$MY&Q-`glYJnGZkp0Lg>hFR(1EU6A9r5^hF`1iAd=i9?WDm5$HISus3Y2R3{6w)1!T1Ed0EswJl1??Ch+G zewq3_?6K@Tnwq`3>>La}sX6tW?A*DTwG1rmyt%2-+E|@b338IBK&oZmAtesq&t^xo z4y{l!cRf3;rP0A%L&HbyNU{3G<{UjL z*j)q8&|udvY3Xz;XdKbnr}4+hG+M||f^n0=(x^?BJrgq&+1#hEO0AHn+VsZDpd)p^ z*?~h2VKt$vSy?`s?Aqp%yQx)Gh41X_*98(hT%0lNtnLiV6`Y*BwG0m0EHx}VJWTxT zw%T0mhLd$UT@y{2ax}5K1spP(bDpf2r|vk%d*!6{4{r79EZX*$k7aVFYm7!t^PH7x z)}{)*4+KnwTsn_PHW<8++icvFxW+(Ns9SWxR$Upsi?Rz3NK9erJKS90VIm^H8pbK0 z%A3i|&dkrxRS+GdRKv;C!^FWiQJHHd6DPkJYi3k=a=8}ncmlgd$9sy_&uvGTx>p#? z=T4kueZBj_2F6H>#-s$3mw8L%xDTyxN@8eA+Fc>Dk5y;m!7GB2`z9{fcv@`VS>Y!a zH8_RXPwd?wtH8p|!KTV($IQ;l!OG3UWu(Qz&d=1v#Kxbg#m%YD%4#JrQ(J|ZxeU8& zN { @@ -49,26 +49,23 @@ document.addEventListener('DOMContentLoaded', () => { ), ); - import(/* webpackChunkName: 'select2' */ 'select2/select2') - .then(() => { - $container - .find('.js-elasticsearch-namespaces') - .select2( - getDropdownConfig( - s__('Elastic|None. Select namespaces to index.'), - Api.namespacesPath, - 'full_path', - ), - ); - $container - .find('.js-elasticsearch-projects') - .select2( - getDropdownConfig( - s__('Elastic|None. Select projects to index.'), - Api.projectsPath, - 'name_with_namespace', - ), - ); - }) - .catch(() => {}); + $container + .find('.js-elasticsearch-namespaces') + .select2( + getDropdownConfig( + s__('Elastic|None. Select namespaces to index.'), + Api.namespacesPath, + 'full_path', + ), + ); + + $container + .find('.js-elasticsearch-projects') + .select2( + getDropdownConfig( + s__('Elastic|None. Select projects to index.'), + Api.projectsPath, + 'name_with_namespace', + ), + ); }); diff --git a/ee/app/views/admin/application_settings/_elasticsearch_form.html.haml b/ee/app/views/admin/application_settings/_elasticsearch_form.html.haml index 227cf81728bfd4..f9eaa25c6d1971 100644 --- a/ee/app/views/admin/application_settings/_elasticsearch_form.html.haml +++ b/ee/app/views/admin/application_settings/_elasticsearch_form.html.haml @@ -50,11 +50,11 @@ .form-group.js-limit-namespaces{ class: ('hidden' unless @application_setting.elasticsearch_limit_indexing) } = f.label :elasticsearch_namespace_ids, _('Namespaces to index'), class: 'label-bold' - = f.text_field :elasticsearch_namespace_ids, class: 'js-elasticsearch-namespaces form-control', value: elasticsearch_namespace_ids, data: { selected: elasticsearch_objects_options(@application_setting.elasticsearch_limited_namespaces(true)).to_json } + = f.text_field :elasticsearch_namespace_ids, class: 'js-elasticsearch-namespaces', value: elasticsearch_namespace_ids, data: { selected: elasticsearch_objects_options(@application_setting.elasticsearch_limited_namespaces(true)).to_json } .form-group.js-limit-projects{ class: ('hidden' unless @application_setting.elasticsearch_limit_indexing) } = f.label :elasticsearch_project_ids, _('Projects to index'), class: 'label-bold' - = f.text_field :elasticsearch_project_ids, class: 'js-elasticsearch-projects form-control', value: elasticsearch_project_ids, data: { selected: elasticsearch_objects_options(@application_setting.elasticsearch_limited_projects(true)).to_json } + = f.text_field :elasticsearch_project_ids, class: 'js-elasticsearch-projects', value: elasticsearch_project_ids, data: { selected: elasticsearch_objects_options(@application_setting.elasticsearch_limited_projects(true)).to_json } .sub-section %h4 Elasticsearch AWS IAM credentials -- GitLab