From 0edb497de9b595ad907491b884f10007c27de5a1 Mon Sep 17 00:00:00 2001 From: Simon Knox <simon@gitlab.com> Date: Wed, 25 Sep 2019 10:51:10 +0000 Subject: [PATCH] Add AlertsService to projects This adds a generic alerts service to a project's settings. Enabling it generates a new token to be used when calling the alert service. --- .rubocop.yml | 6 + ee/app/models/ee/project.rb | 22 ++-- ee/app/models/ee/service.rb | 1 + .../models/project_services/alerts_service.rb | 78 ++++++++++++ .../project_services/alerts_service_data.rb | 14 +++ .../projects/services/alerts/_help.html.haml | 2 +- .../unreleased/generic-alertsettings-befe.yml | 5 + ee/lib/ee/api/helpers/services_helpers.rb | 1 + .../services/user_activates_alerts_spec.rb | 112 ++++++++++++++++++ .../project_services/alerts_service_spec.rb | 109 +++++++++++++++++ ee/spec/models/project_spec.rb | 15 +++ ee/spec/models/service_spec.rb | 11 +- lib/api/helpers/services_helpers.rb | 2 + locale/gitlab.pot | 6 + spec/lib/gitlab/import_export/all_models.yml | 1 + .../services_shared_context.rb | 28 ++++- 16 files changed, 396 insertions(+), 17 deletions(-) create mode 100644 ee/app/models/project_services/alerts_service.rb create mode 100644 ee/app/models/project_services/alerts_service_data.rb create mode 100644 ee/changelogs/unreleased/generic-alertsettings-befe.yml create mode 100644 ee/spec/features/projects/services/user_activates_alerts_spec.rb create mode 100644 ee/spec/models/project_services/alerts_service_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 693ee0ae8473..45464ca3318a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -218,6 +218,12 @@ ActiveRecordAssociationReload: - 'spec/**/*' - 'ee/spec/**/*' +Naming/PredicateName: + Enabled: true + Exclude: + - 'spec/**/*' + - 'ee/spec/**/*' + RSpec/FactoriesInMigrationSpecs: Enabled: true Include: diff --git a/ee/app/models/ee/project.rb b/ee/app/models/ee/project.rb index ad8542706088..71e47e638cf6 100644 --- a/ee/app/models/ee/project.rb +++ b/ee/app/models/ee/project.rb @@ -39,10 +39,13 @@ module Project has_one :project_registry, class_name: 'Geo::ProjectRegistry', inverse_of: :project has_one :push_rule, ->(project) { project&.feature_available?(:push_rules) ? all : none } has_one :index_status + has_one :jenkins_service has_one :jenkins_deprecated_service has_one :github_service has_one :gitlab_slack_application_service + has_one :alerts_service + has_one :tracing_setting, class_name: 'ProjectTracingSetting' has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting' has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting' @@ -487,17 +490,11 @@ def merge_requests_ff_only_enabled override :disabled_services def disabled_services strong_memoize(:disabled_services) do - disabled_services = [] - - unless feature_available?(:jenkins_integration) - disabled_services.push('jenkins', 'jenkins_deprecated') - end - - unless feature_available?(:github_project_service_integration) - disabled_services.push('github') + [].tap do |services| + services.push('jenkins', 'jenkins_deprecated') unless feature_available?(:jenkins_integration) + services.push('github') unless feature_available?(:github_project_service_integration) + services.push('alerts') unless alerts_service_available? end - - disabled_services end end @@ -613,6 +610,11 @@ def design_repository @design_repository ||= DesignManagement::Repository.new(self) end + def alerts_service_available? + ::Feature.enabled?(:generic_alert_endpoint, self) && + feature_available?(:incident_management) + end + def package_already_taken?(package_name) namespace.root_ancestor.all_projects .joins(:packages) diff --git a/ee/app/models/ee/service.rb b/ee/app/models/ee/service.rb index e6753d8cbf6d..9bd0f6b24007 100644 --- a/ee/app/models/ee/service.rb +++ b/ee/app/models/ee/service.rb @@ -13,6 +13,7 @@ def available_services_names github jenkins jenkins_deprecated + alerts ] if ::Gitlab.dev_env_or_com? diff --git a/ee/app/models/project_services/alerts_service.rb b/ee/app/models/project_services/alerts_service.rb new file mode 100644 index 000000000000..2f7902d96176 --- /dev/null +++ b/ee/app/models/project_services/alerts_service.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'securerandom' + +class AlertsService < Service + has_one :data, class_name: 'AlertsServiceData', autosave: true, + inverse_of: :service, foreign_key: :service_id + + attribute :token, :string + delegate :token, :token=, :token_changed?, :token_was, to: :data + + validates :token, presence: true, if: :activated? + + before_validation :prevent_token_assignment + before_validation :ensure_token, if: :activated? + + def url + url_helpers.project_alerts_notify_url(project, format: :json) + end + + def json_fields + super + %w(token) + end + + def editable? + false + end + + def show_active_box? + false + end + + def can_test? + false + end + + def title + _('Alerts endpoint') + end + + def description + _('Receive alerts on GitLab from any source') + end + + def detailed_description + description + end + + def self.to_param + 'alerts' + end + + def self.supported_events + %w() + end + + def data + super || build_data + end + + private + + def prevent_token_assignment + self.token = token_was if token.present? && token_changed? + end + + def ensure_token + self.token = generate_token if token.blank? + end + + def generate_token + SecureRandom.hex + end + + def url_helpers + Gitlab::Routing.url_helpers + end +end diff --git a/ee/app/models/project_services/alerts_service_data.rb b/ee/app/models/project_services/alerts_service_data.rb new file mode 100644 index 000000000000..5a52ed83455e --- /dev/null +++ b/ee/app/models/project_services/alerts_service_data.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'securerandom' + +class AlertsServiceData < ApplicationRecord + belongs_to :service, class_name: 'AlertsService' + + validates :service, presence: true + + attr_encrypted :token, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm' +end diff --git a/ee/app/views/projects/services/alerts/_help.html.haml b/ee/app/views/projects/services/alerts/_help.html.haml index 1fa0350665eb..acab6c75e657 100644 --- a/ee/app/views/projects/services/alerts/_help.html.haml +++ b/ee/app/views/projects/services/alerts/_help.html.haml @@ -1,4 +1,4 @@ -- return unless Feature.enabled?(:generic_alert_endpoint, @project) +- return unless @project.alerts_service_available? .js-alerts-service-settings{ data: { activated: @service.activated?.to_s, form_path: project_service_path(@project, @service.to_param), diff --git a/ee/changelogs/unreleased/generic-alertsettings-befe.yml b/ee/changelogs/unreleased/generic-alertsettings-befe.yml new file mode 100644 index 000000000000..9680e25ed974 --- /dev/null +++ b/ee/changelogs/unreleased/generic-alertsettings-befe.yml @@ -0,0 +1,5 @@ +--- +title: Add Alerts Service to Projects +merge_request: 16117 +author: +type: added diff --git a/ee/lib/ee/api/helpers/services_helpers.rb b/ee/lib/ee/api/helpers/services_helpers.rb index 34b1129dc3dc..9210828d7c7a 100644 --- a/ee/lib/ee/api/helpers/services_helpers.rb +++ b/ee/lib/ee/api/helpers/services_helpers.rb @@ -81,6 +81,7 @@ def service_classes ::GithubService, ::JenkinsService, ::JenkinsDeprecatedService, + ::AlertsService, *super ] end diff --git a/ee/spec/features/projects/services/user_activates_alerts_spec.rb b/ee/spec/features/projects/services/user_activates_alerts_spec.rb new file mode 100644 index 000000000000..2d1cf0660de3 --- /dev/null +++ b/ee/spec/features/projects/services/user_activates_alerts_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'User activates Alerts' do + set(:project) { create(:project) } + set(:user) { create(:user) } + + let(:service_name) { 'alerts' } + let(:service_title) { 'Alerts endpoint' } + + before do + sign_in(user) + project.add_maintainer(user) + end + + shared_examples 'no service' do + it 'cannot see the service' do + visit_project_services + + expect(page).not_to have_link(service_title) + end + end + + context 'when feature available', :js do + before do + stub_licensed_features(incident_management: true) + stub_feature_flags(generic_alert_endpoint: true) + end + + context 'when service is deactivated' do + it 'activates service' do + visit_project_services + + expect(page).to have_link(service_title) + click_link(service_title) + + expect(page).not_to have_active_service + + click_activate_service + wait_for_requests + + expect(page).to have_active_service + end + end + + context 'when service is activated' do + before do + visit_alerts_service + click_activate_service + end + + it 're-generates key' do + expect(reset_key.value).to be_blank + + click_reset_key + click_confirm_reset_key + wait_for_requests + + expect(reset_key.value).to be_present + end + end + + context 'when feature flag `generic_alert_endpoint` disabled' do + before do + stub_feature_flags(generic_alert_endpoint: false) + end + + it_behaves_like 'no service' + end + end + + context 'when feature unavailable' do + before do + stub_licensed_features(incident_management: false) + end + + it_behaves_like 'no service' + end + + private + + def visit_project_services + visit(project_settings_integrations_path(project)) + end + + def visit_alerts_service + visit(edit_project_service_path(project, service_name)) + end + + def click_activate_service + find('#activated').click + end + + def click_reset_key + click_button('Reset key') + end + + def click_confirm_reset_key + within '.modal-content' do + click_reset_key + end + end + + def reset_key + find_field('Authorization key') + end + + def have_active_service + have_selector('.js-service-active-status[data-value="true"]') + end +end diff --git a/ee/spec/models/project_services/alerts_service_spec.rb b/ee/spec/models/project_services/alerts_service_spec.rb new file mode 100644 index 000000000000..12c5d0ea91ed --- /dev/null +++ b/ee/spec/models/project_services/alerts_service_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AlertsService do + set(:project) { create(:project) } + let(:service_params) { { project: project, active: active } } + let(:active) { true } + let(:service) { described_class.new(service_params) } + + shared_context 'when active' do + let(:active) { true } + end + + shared_context 'when inactive' do + let(:active) { false } + end + + shared_context 'when persisted' do + before do + service.save! + service.reload + end + end + + describe '#url' do + include Gitlab::Routing + + subject { service.url } + + it { is_expected.to eq(project_alerts_notify_url(project, format: :json)) } + end + + describe '#json_fields' do + subject { service.json_fields } + + it { is_expected.to eq(%w(active token)) } + end + + describe '#as_json' do + subject { service.as_json(only: service.json_fields) } + + it { is_expected.to eq('active' => true, 'token' => nil) } + end + + describe '#token' do + shared_context 'reset token' do + before do + service.token = '' + service.valid? + end + end + + shared_context 'assign token' do |token| + before do + service.token = token + service.valid? + end + end + + shared_examples 'valid token' do + it { is_expected.to match(/\A\h{32}\z/) } + end + + shared_examples 'no token' do + it { is_expected.to be_blank } + end + + subject { service.token } + + context 'when active' do + include_context 'when active' + + context 'when resetting' do + let!(:previous_token) { service.token } + + include_context 'reset token' + + it_behaves_like 'valid token' + + it { is_expected.not_to eq(previous_token) } + end + + context 'when assigning' do + include_context 'assign token', 'random token' + + it_behaves_like 'valid token' + end + end + + context 'when inactive' do + include_context 'when inactive' + + context 'when resetting' do + let!(:previous_token) { service.token } + + include_context 'reset token' + + it_behaves_like 'no token' + end + end + + context 'when persisted' do + include_context 'when persisted' + + it_behaves_like 'valid token' + end + end +end diff --git a/ee/spec/models/project_spec.rb b/ee/spec/models/project_spec.rb index ebd27bf9e9ab..42baa45ebaff 100644 --- a/ee/spec/models/project_spec.rb +++ b/ee/spec/models/project_spec.rb @@ -970,6 +970,7 @@ where(:license_feature, :disabled_services) do :jenkins_integration | %w(jenkins jenkins_deprecated) :github_project_service_integration | %w(github) + :incident_management | %w(alerts) end with_them do @@ -989,6 +990,20 @@ it { is_expected.to include(*disabled_services) } end end + + context 'when incident_management is available' do + before do + stub_licensed_features(incident_management: true) + end + + context 'when feature flag generic_alert_endpoint is disabled' do + before do + stub_feature_flags(generic_alert_endpoint: false) + end + + it { is_expected.to include('alerts') } + end + end end describe '#pull_mirror_available?' do diff --git a/ee/spec/models/service_spec.rb b/ee/spec/models/service_spec.rb index 9d8cfa59d595..eeb0750b4b0d 100644 --- a/ee/spec/models/service_spec.rb +++ b/ee/spec/models/service_spec.rb @@ -4,6 +4,15 @@ describe Service do describe 'Available services' do - it { expect(described_class.available_services_names).to include("jenkins", "jira") } + let(:ee_services) do + %w( + github + jenkins + jenkins_deprecated + alerts + ) + end + + it { expect(described_class.available_services_names).to include(*ee_services) } end end diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb index eba4ebb4b6e0..2475e384a50a 100644 --- a/lib/api/helpers/services_helpers.rb +++ b/lib/api/helpers/services_helpers.rb @@ -155,6 +155,7 @@ def self.chat_notification_events def self.services { + 'alerts' => [], 'asana' => [ { required: true, @@ -696,6 +697,7 @@ def self.services def self.service_classes [ + ::AlertsService, ::AsanaService, ::AssemblaService, ::BambooService, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5ac4a2f1e32c..ca62f137ac3d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1234,6 +1234,9 @@ msgstr "" msgid "Alerts" msgstr "" +msgid "Alerts endpoint" +msgstr "" + msgid "All" msgstr "" @@ -12760,6 +12763,9 @@ msgstr "" msgid "Receive alerts from manually configured Prometheus servers." msgstr "" +msgid "Receive alerts on GitLab from any source" +msgstr "" + msgid "Receive notifications about your own activity" msgstr "" diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 3315dd3b974a..d9272afadf2e 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -411,6 +411,7 @@ project: - project_aliases - external_pull_requests - pages_metadatum +- alerts_service award_emoji: - awardable - user diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb index 4d176ab5fcab..113bcc2af9cc 100644 --- a/spec/support/shared_contexts/services_shared_context.rb +++ b/spec/support/shared_contexts/services_shared_context.rb @@ -28,12 +28,17 @@ end end + let(:licensed_features) do + { + 'github' => :github_project_service_integration, + 'jenkins' => :jenkins_integration, + 'jenkins_deprecated' => :jenkins_integration, + 'alerts' => :incident_management + } + end + before do - if service == 'github' && respond_to?(:stub_licensed_features) - stub_licensed_features(github_project_service_integration: true) - project.clear_memoization(:disabled_services) - project.clear_memoization(:licensed_feature_available) - end + enable_license_for_service(service) end def initialize_service(service) @@ -42,5 +47,18 @@ def initialize_service(service) service_item.save! service_item end + + private + + def enable_license_for_service(service) + return unless respond_to?(:stub_licensed_features) + + licensed_feature = licensed_features[service] + return unless licensed_feature + + stub_licensed_features(licensed_feature => true) + project.clear_memoization(:disabled_services) + project.clear_memoization(:licensed_feature_available) + end end end -- GitLab