Commit 072e9f54 authored by Sean McGivern's avatar Sean McGivern

Merge branch '9258-support-alerts-from-external-prometheus-servers-model' into 'master'

Implement alerting setting model

See merge request gitlab-org/gitlab-ee!9334
parents fc3cb7aa 39aa9458
...@@ -2166,6 +2166,11 @@ ActiveRecord::Schema.define(version: 20190124200344) do ...@@ -2166,6 +2166,11 @@ ActiveRecord::Schema.define(version: 20190124200344) do
t.index ["name"], name: "index_programming_languages_on_name", unique: true, using: :btree t.index ["name"], name: "index_programming_languages_on_name", unique: true, using: :btree
end end
create_table "project_alerting_settings", primary_key: "project_id", id: :integer, force: :cascade do |t|
t.string "encrypted_token", null: false
t.string "encrypted_token_iv", null: false
end
create_table "project_authorizations", id: false, force: :cascade do |t| create_table "project_authorizations", id: false, force: :cascade do |t|
t.integer "user_id", null: false t.integer "user_id", null: false
t.integer "project_id", null: false t.integer "project_id", null: false
...@@ -3462,6 +3467,7 @@ ActiveRecord::Schema.define(version: 20190124200344) do ...@@ -3462,6 +3467,7 @@ ActiveRecord::Schema.define(version: 20190124200344) do
add_foreign_key "personal_access_tokens", "users" add_foreign_key "personal_access_tokens", "users"
add_foreign_key "pool_repositories", "projects", column: "source_project_id", on_delete: :nullify add_foreign_key "pool_repositories", "projects", column: "source_project_id", on_delete: :nullify
add_foreign_key "pool_repositories", "shards", on_delete: :restrict add_foreign_key "pool_repositories", "shards", on_delete: :restrict
add_foreign_key "project_alerting_settings", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "projects", on_delete: :cascade add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "project_auto_devops", "projects", on_delete: :cascade add_foreign_key "project_auto_devops", "projects", on_delete: :cascade
......
...@@ -8,13 +8,34 @@ module EE ...@@ -8,13 +8,34 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
before_action :authorize_read_prometheus_alerts!,
only: [:reset_alerting_token]
respond_to :json, only: [:reset_alerting_token]
def reset_alerting_token
result = ::Projects::Operations::UpdateService
.new(project, current_user, alerting_params)
.execute
if result[:status] == :success
render json: { token: project.alerting_setting.token }
else
render json: {}, status: :unprocessable_entity
end
end
helper_method :tracing_setting helper_method :tracing_setting
private
def alerting_params
{ alerting_setting_attributes: { regenerate_token: true } }
end
def tracing_setting def tracing_setting
@tracing_setting ||= project.tracing_setting || project.build_tracing_setting @tracing_setting ||= project.tracing_setting || project.build_tracing_setting
end end
private :tracing_setting
end end
override :permitted_project_params override :permitted_project_params
......
# frozen_string_literal: true
require 'securerandom'
module Alerting
class ProjectAlertingSetting < ApplicationRecord
belongs_to :project
validates :token, presence: true
attr_encrypted :token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-gcm'
before_validation :ensure_token
private
def ensure_token
self.token ||= generate_token
end
def generate_token
SecureRandom.hex
end
end
end
...@@ -40,6 +40,7 @@ module EE ...@@ -40,6 +40,7 @@ module EE
has_one :github_service has_one :github_service
has_one :gitlab_slack_application_service has_one :gitlab_slack_application_service
has_one :tracing_setting, class_name: 'ProjectTracingSetting' has_one :tracing_setting, class_name: 'ProjectTracingSetting'
has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting'
has_one :feature_usage, class_name: 'ProjectFeatureUsage' has_one :feature_usage, class_name: 'ProjectFeatureUsage'
has_many :reviews, inverse_of: :project has_many :reviews, inverse_of: :project
...@@ -118,6 +119,7 @@ module EE ...@@ -118,6 +119,7 @@ module EE
delegate :store_security_reports_available?, to: :namespace delegate :store_security_reports_available?, to: :namespace
accepts_nested_attributes_for :tracing_setting, update_only: true, allow_destroy: true accepts_nested_attributes_for :tracing_setting, update_only: true, allow_destroy: true
accepts_nested_attributes_for :alerting_setting, update_only: true
end end
class_methods do class_methods do
......
...@@ -9,7 +9,9 @@ module EE ...@@ -9,7 +9,9 @@ module EE
override :project_update_params override :project_update_params
def project_update_params def project_update_params
super.merge(tracing_setting_params) super
.merge(tracing_setting_params)
.merge(alerting_setting_params)
end end
private private
...@@ -22,6 +24,23 @@ module EE ...@@ -22,6 +24,23 @@ module EE
{ tracing_setting_attributes: attr.merge(_destroy: destroy) } { tracing_setting_attributes: attr.merge(_destroy: destroy) }
end end
def alerting_setting_params
return {} unless can?(current_user, :read_prometheus_alerts, project)
attr = params[:alerting_setting_attributes]
return {} unless attr
regenerate_token = attr.delete(:regenerate_token)
if regenerate_token
attr[:token] = nil
else
attr = attr.except(:token) # rubocop: disable CodeReuse/ActiveRecord
end
{ alerting_setting_attributes: attr }
end
end end
end end
end end
......
...@@ -34,10 +34,21 @@ module Projects ...@@ -34,10 +34,21 @@ module Projects
end end
def valid_alert_manager_token?(token) def valid_alert_manager_token?(token)
# We don't support token authorization for manual installations. valid_for_manual?(token) || valid_for_managed?(token)
end
def valid_for_manual?(token)
prometheus = project.find_or_initialize_service('prometheus') prometheus = project.find_or_initialize_service('prometheus')
return true if prometheus.manual_configuration? return false unless prometheus.manual_configuration?
if setting = project.alerting_setting
compare_token(token, setting.token)
else
token.nil?
end
end
def valid_for_managed?(token)
prometheus_application = available_prometheus_application(project) prometheus_application = available_prometheus_application(project)
return false unless prometheus_application return false unless prometheus_application
......
---
title: Support alerts from external Prometheus servers
merge_request: 9334
author:
type: added
...@@ -36,6 +36,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -36,6 +36,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
end end
end end
namespace :settings do
resource :operations, only: [:show, :update] do
member do
post :reset_alerting_token
end
end
end
end end
end end
end end
......
# frozen_string_literal: true
class CreateProjectAlertingSettings < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :project_alerting_settings, id: :int, primary_key: :project_id do |t|
t.string :encrypted_token, null: false
t.string :encrypted_token_iv, null: false
t.foreign_key :projects, column: :project_id, on_delete: :cascade
end
end
end
...@@ -232,6 +232,107 @@ describe Projects::Settings::OperationsController do ...@@ -232,6 +232,107 @@ describe Projects::Settings::OperationsController do
end end
end end
describe 'POST reset_alerting_token' do
let(:project) { create(:project) }
before do
stub_licensed_features(prometheus_alerts: true)
project.add_maintainer(user)
end
context 'with existing alerting setting' do
let!(:alerting_setting) do
create(:project_alerting_setting, project: project)
end
let!(:old_token) { alerting_setting.token }
it 'returns newly reset token' do
reset_alerting_token
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['token']).to eq(alerting_setting.reload.token)
expect(old_token).not_to eq(alerting_setting.token)
end
end
context 'without existing alerting setting' do
it 'creates a token' do
reset_alerting_token
expect(response).to have_gitlab_http_status(:ok)
expect(project.alerting_setting).not_to be_nil
expect(json_response['token']).to eq(project.alerting_setting.token)
end
end
context 'when update fails' do
let(:operations_update_service) { spy(:operations_update_service) }
let(:alerting_params) do
{ alerting_setting_attributes: { regenerate_token: true } }
end
before do
expect(::Projects::Operations::UpdateService)
.to receive(:new).with(project, user, alerting_params)
.and_return(operations_update_service)
expect(operations_update_service).to receive(:execute)
.and_return(status: :error)
end
it 'returns unprocessable_entity' do
reset_alerting_token
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to be_empty
end
end
context 'with insufficient permissions' do
before do
project.add_reporter(user)
end
it 'returns 404' do
reset_alerting_token
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'as an anonymous user' do
before do
sign_out(user)
end
it 'returns unauthorized status' do
reset_alerting_token
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'without a license' do
before do
stub_licensed_features(prometheus_alerts: false)
end
it 'returns 404' do
reset_alerting_token
expect(response).to have_gitlab_http_status(:not_found)
end
end
private
def reset_alerting_token
post :reset_alerting_token,
params: project_params(project),
format: :json
end
end
private private
def project_params(project, params = {}) def project_params(project, params = {})
......
# frozen_string_literal: true
FactoryBot.define do
factory :project_alerting_setting, class: Alerting::ProjectAlertingSetting do
project
token 'access_token_123'
end
end
...@@ -77,6 +77,7 @@ project: ...@@ -77,6 +77,7 @@ project:
- packages - packages
- package_files - package_files
- tracing_setting - tracing_setting
- alerting_setting
- webide_pipelines - webide_pipelines
- reviews - reviews
prometheus_metrics: prometheus_metrics:
......
# frozen_string_literal: true
require 'spec_helper'
describe Alerting::ProjectAlertingSetting do
set(:project) { create(:project) }
subject { create(:project_alerting_setting, project: project) }
describe 'Associations' do
it { is_expected.to belong_to(:project) }
end
describe '#token' do
context 'when set' do
let(:token) { SecureRandom.hex }
subject do
create(:project_alerting_setting, project: project, token: token)
end
it 'reads the token' do
expect(subject.token).to eq(token)
expect(subject.encrypted_token).not_to be_nil
expect(subject.encrypted_token_iv).not_to be_nil
end
end
context 'when not set' do
before do
subject.token = nil
end
it 'generates a token before validation' do
expect(subject).to be_valid
expect(subject.token).to match(/\A\h{32}\z/)
end
end
end
end
...@@ -16,6 +16,7 @@ describe Project do ...@@ -16,6 +16,7 @@ describe Project do
it { is_expected.to have_one(:import_state).class_name('ProjectImportState') } it { is_expected.to have_one(:import_state).class_name('ProjectImportState') }
it { is_expected.to have_one(:repository_state).class_name('ProjectRepositoryState').inverse_of(:project) } it { is_expected.to have_one(:repository_state).class_name('ProjectRepositoryState').inverse_of(:project) }
it { is_expected.to have_one(:alerting_setting).class_name('Alerting::ProjectAlertingSetting') }
it { is_expected.to have_many(:reviews).inverse_of(:project) } it { is_expected.to have_many(:reviews).inverse_of(:project) }
it { is_expected.to have_many(:path_locks) } it { is_expected.to have_many(:path_locks) }
......
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
require 'spec_helper' require 'spec_helper'
describe Projects::Operations::UpdateService do describe Projects::Operations::UpdateService do
subject { described_class.new(project, user, params) } set(:user) { create(:user) }
let(:project) { create(:project) }
let(:result) { subject.execute } let(:result) { subject.execute }
set(:user) { create(:user) } subject { described_class.new(project, user, params) }
set(:project) { create(:project) }
describe '#execute' do describe '#execute' do
context 'tracing setting' do context 'tracing setting' do
...@@ -98,5 +98,95 @@ describe Projects::Operations::UpdateService do ...@@ -98,5 +98,95 @@ describe Projects::Operations::UpdateService do
end end
end end
end end
context 'alerting setting' do
before do
stub_licensed_features(prometheus_alerts: true)
project.add_maintainer(user)
end
shared_examples 'no operation' do
it 'does nothing' do
expect(result[:status]).to eq(:success)
expect(project.reload.alerting_setting).to be_nil
end
end
context 'with valid params' do
let(:params) { { alerting_setting_attributes: alerting_params } }
shared_examples 'setting creation' do
it 'creates a setting' do
expect(project.alerting_setting).to be_nil
expect(result[:status]).to eq(:success)
expect(project.reload.alerting_setting).not_to be_nil
end
end
context 'when regenerate_token is not set' do
let(:alerting_params) { { token: 'some token' } }
context 'with an existing setting' do
let!(:alerting_setting) do
create(:project_alerting_setting, project: project)
end
it 'ignores provided token' do
expect(result[:status]).to eq(:success)
expect(project.reload.alerting_setting.token)
.to eq(alerting_setting.token)
end
end
context 'without an existing setting' do
it_behaves_like 'setting creation'
end
end
context 'when regenerate_token is set' do
let(:alerting_params) { { regenerate_token: true } }
context 'with an existing setting' do
let(:token) { 'some token' }
let!(:alerting_setting) do
create(:project_alerting_setting, project: project, token: token)
end
it 'regenerates token' do
expect(result[:status]).to eq(:success)
expect(project.reload.alerting_setting.token).not_to eq(token)
end
end
context 'without an existing setting' do
it_behaves_like 'setting creation'
context 'without license' do
before do
stub_licensed_features(prometheus_alerts: false)
end
it_behaves_like 'no operation'
end
context 'with insufficient permissions' do
before do
project.add_reporter(user)
end
it_behaves_like 'no operation'
end
end
end
end
context 'with empty params' do
let(:params) { {} }
it_behaves_like 'no operation'
end
end
end end
end end
...@@ -70,8 +70,6 @@ describe Projects::Prometheus::Alerts::NotifyService do ...@@ -70,8 +70,6 @@ describe Projects::Prometheus::Alerts::NotifyService do
end end
with_them do with_them do
let(:alert_manager_token) { token_input }
before do before do
cluster = create(:cluster, :provided_by_user, cluster = create(:cluster, :provided_by_user,
projects: [project], projects: [project],
...@@ -138,11 +136,38 @@ describe Projects::Prometheus::Alerts::NotifyService do ...@@ -138,11 +136,38 @@ describe Projects::Prometheus::Alerts::NotifyService do
end end
context 'with manual prometheus installation' do context 'with manual prometheus installation' do
before do using RSpec::Parameterized::TableSyntax
create(:prometheus_service, project: project)
where(:alerting_setting, :configured_token, :token_input, :result) do
true | token | token | :success
true | token | 'x' | :failure
true | token | nil | :failure
false | nil | nil | :success
false | nil | token | :failure
end end
it_behaves_like 'notifies alerts' with_them do
let(:alert_manager_token) { token_input }
before do
create(:prometheus_service, project: project)
if alerting_setting
create(:project_alerting_setting,
project: project,
token: configured_token)
end
end
case result = params[:result]
when :success
it_behaves_like 'notifies alerts'
when :failure
it_behaves_like 'no notifications'
else
raise "invalid result: #{result.inspect}"
end
end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment