Commit 1722caac authored by Sean Arnold's avatar Sean Arnold Committed by Mayra Cabrera

Add Alert validations, enums, factory, specs

- Move model to under AlertManagement module
- Fix migration
parent faec4fc1
# frozen_string_literal: true
module AlertManagement
class Alert < ApplicationRecord
include AtomicInternalId
include ShaAttribute
belongs_to :project
belongs_to :issue, optional: true
has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) }
self.table_name = 'alert_management_alerts'
sha_attribute :fingerprint
HOSTS_MAX_LENGTH = 255
validates :title, length: { maximum: 200 }, presence: true
validates :description, length: { maximum: 1_000 }
validates :service, length: { maximum: 100 }
validates :monitoring_tool, length: { maximum: 100 }
validates :project, presence: true
validates :events, presence: true
validates :severity, presence: true
validates :status, presence: true
validates :started_at, presence: true
validates :fingerprint, uniqueness: { scope: :project }, allow_blank: true
validate :hosts_length
enum severity: {
critical: 0,
high: 1,
medium: 2,
low: 3,
info: 4,
unknown: 5
}
enum status: {
triggered: 0,
acknowledged: 1,
resolved: 2,
ignored: 3
}
def fingerprint=(value)
if value.blank?
super(nil)
else
super(Digest::SHA1.hexdigest(value.to_s))
end
end
private
def hosts_length
return unless hosts
errors.add(:hosts, "hosts array is over #{HOSTS_MAX_LENGTH} chars") if hosts.join.length > HOSTS_MAX_LENGTH
end
end
end
......@@ -3,7 +3,7 @@
module InternalIdEnums
def self.usage_resources
# when adding new resource, make sure it doesn't conflict with EE usage_resources
{ issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6, operations_user_lists: 7 }
{ issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6, operations_user_lists: 7, alert_management_alerts: 8 }
end
end
......
......@@ -48,6 +48,7 @@ class Issue < ApplicationRecord
has_many :sent_notifications, as: :noteable
has_one :sentry_issue
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
accepts_nested_attributes_for :sentry_issue
......
......@@ -255,6 +255,8 @@ class Project < ApplicationRecord
has_many :prometheus_alert_events, inverse_of: :project
has_many :self_managed_prometheus_alert_events, inverse_of: :project
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :project
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
# here.
......
---
title: Add table for Alert Management alerts
merge_request: 29864
author:
type: added
# frozen_string_literal: true
class CreateAlertManagementAlerts < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
unless table_exists?(:alert_management_alerts)
create_table :alert_management_alerts do |t|
t.timestamps_with_timezone
t.datetime_with_timezone :started_at, null: false
t.datetime_with_timezone :ended_at
t.integer :events, default: 1, null: false
t.integer :iid, null: false
t.integer :severity, default: 0, null: false, limit: 2
t.integer :status, default: 0, null: false, limit: 2
t.binary :fingerprint
t.bigint :issue_id, index: true
t.bigint :project_id, null: false
t.text :title, null: false
t.text :description
t.text :service
t.text :monitoring_tool
t.text :hosts, array: true, null: false, default: [] # rubocop:disable Migration/AddLimitToTextColumns
t.jsonb :payload, null: false, default: {}
t.index %w(project_id iid), name: 'index_alert_management_alerts_on_project_id_and_iid', unique: true, using: :btree
t.index %w(project_id fingerprint), name: 'index_alert_management_alerts_on_project_id_and_fingerprint', unique: true, using: :btree
end
end
add_text_limit :alert_management_alerts, :title, 200
add_text_limit :alert_management_alerts, :description, 1000
add_text_limit :alert_management_alerts, :service, 100
add_text_limit :alert_management_alerts, :monitoring_tool, 100
end
def down
drop_table :alert_management_alerts
end
end
# frozen_string_literal: true
class AddForeignKeysForAlertManagementAlerts < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key :alert_management_alerts, :projects, column: :project_id, on_delete: :cascade
add_concurrent_foreign_key :alert_management_alerts, :issues, column: :issue_id, on_delete: :nullify
end
def down
remove_foreign_key_if_exists :alert_management_alerts, column: :project_id
remove_foreign_key_if_exists :alert_management_alerts, column: :issue_id
end
end
......@@ -24,6 +24,40 @@ CREATE SEQUENCE public.abuse_reports_id_seq
ALTER SEQUENCE public.abuse_reports_id_seq OWNED BY public.abuse_reports.id;
CREATE TABLE public.alert_management_alerts (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
started_at timestamp with time zone NOT NULL,
ended_at timestamp with time zone,
events integer DEFAULT 1 NOT NULL,
iid integer NOT NULL,
severity smallint DEFAULT 0 NOT NULL,
status smallint DEFAULT 0 NOT NULL,
fingerprint bytea,
issue_id bigint,
project_id bigint NOT NULL,
title text NOT NULL,
description text,
service text,
monitoring_tool text,
hosts text[] DEFAULT '{}'::text[] NOT NULL,
payload jsonb DEFAULT '{}'::jsonb NOT NULL,
CONSTRAINT check_2df3e2fdc1 CHECK ((char_length(monitoring_tool) <= 100)),
CONSTRAINT check_5e9e57cadb CHECK ((char_length(description) <= 1000)),
CONSTRAINT check_bac14dddde CHECK ((char_length(service) <= 100)),
CONSTRAINT check_d1d1c2d14c CHECK ((char_length(title) <= 200))
);
CREATE SEQUENCE public.alert_management_alerts_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.alert_management_alerts_id_seq OWNED BY public.alert_management_alerts.id;
CREATE TABLE public.alerts_service_data (
id bigint NOT NULL,
service_id integer NOT NULL,
......@@ -7014,6 +7048,8 @@ ALTER SEQUENCE public.zoom_meetings_id_seq OWNED BY public.zoom_meetings.id;
ALTER TABLE ONLY public.abuse_reports ALTER COLUMN id SET DEFAULT nextval('public.abuse_reports_id_seq'::regclass);
ALTER TABLE ONLY public.alert_management_alerts ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alerts_id_seq'::regclass);
ALTER TABLE ONLY public.alerts_service_data ALTER COLUMN id SET DEFAULT nextval('public.alerts_service_data_id_seq'::regclass);
ALTER TABLE ONLY public.allowed_email_domains ALTER COLUMN id SET DEFAULT nextval('public.allowed_email_domains_id_seq'::regclass);
......@@ -7623,6 +7659,9 @@ ALTER TABLE ONLY public.zoom_meetings ALTER COLUMN id SET DEFAULT nextval('publi
ALTER TABLE ONLY public.abuse_reports
ADD CONSTRAINT abuse_reports_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.alert_management_alerts
ADD CONSTRAINT alert_management_alerts_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.alerts_service_data
ADD CONSTRAINT alerts_service_data_pkey PRIMARY KEY (id);
......@@ -8686,6 +8725,12 @@ CREATE UNIQUE INDEX idx_vulnerability_issue_links_on_vulnerability_id_and_link_t
CREATE INDEX index_abuse_reports_on_user_id ON public.abuse_reports USING btree (user_id);
CREATE INDEX index_alert_management_alerts_on_issue_id ON public.alert_management_alerts USING btree (issue_id);
CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_fingerprint ON public.alert_management_alerts USING btree (project_id, fingerprint);
CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_iid ON public.alert_management_alerts USING btree (project_id, iid);
CREATE INDEX index_alerts_service_data_on_service_id ON public.alerts_service_data USING btree (service_id);
CREATE INDEX index_allowed_email_domains_on_group_id ON public.allowed_email_domains USING btree (group_id);
......@@ -10641,6 +10686,9 @@ ALTER TABLE ONLY public.geo_container_repository_updated_events
ALTER TABLE ONLY public.users_star_projects
ADD CONSTRAINT fk_22cd27ddfc FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.alert_management_alerts
ADD CONSTRAINT fk_2358b75436 FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.ci_stages
ADD CONSTRAINT fk_2360681d1d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
......@@ -10896,6 +10944,9 @@ ALTER TABLE ONLY public.issues
ALTER TABLE ONLY public.epics
ADD CONSTRAINT fk_9d480c64b2 FOREIGN KEY (start_date_sourcing_epic_id) REFERENCES public.epics(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.alert_management_alerts
ADD CONSTRAINT fk_9e49e5c2b7 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.ci_pipeline_schedules
ADD CONSTRAINT fk_9ea99f58d2 FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE SET NULL;
......@@ -13264,5 +13315,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200416111111
20200416120128
20200416120354
20200417044453
20200421233150
\.
# frozen_string_literal: true
require 'ffaker'
FactoryBot.define do
factory :alert_management_alert, class: 'AlertManagement::Alert' do
project
title { FFaker::Lorem.sentence }
started_at { Time.current }
trait :with_issue do
issue
end
trait :with_fingerprint do
fingerprint { SecureRandom.hex }
end
trait :with_service do
service { FFaker::App.name }
end
trait :with_monitoring_tool do
monitoring_tool { FFaker::App.name }
end
trait :with_host do
hosts { FFaker::Internet.public_ip_v4_address }
end
trait :resolved do
status { :resolved }
end
end
end
......@@ -39,6 +39,7 @@ issues:
- related_vulnerabilities
- user_mentions
- system_note_metadata
- alert_management_alert
events:
- author
- project
......@@ -481,6 +482,7 @@ project:
- daily_report_results
- jira_imports
- compliance_framework_setting
- alert_management_alerts
award_emoji:
- awardable
- user
......
# frozen_string_literal: true
require 'spec_helper'
describe AlertManagement::Alert do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:issue) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:events) }
it { is_expected.to validate_presence_of(:severity) }
it { is_expected.to validate_presence_of(:status) }
it { is_expected.to validate_presence_of(:started_at) }
it { is_expected.to validate_length_of(:title).is_at_most(200) }
it { is_expected.to validate_length_of(:description).is_at_most(1000) }
it { is_expected.to validate_length_of(:service).is_at_most(100) }
it { is_expected.to validate_length_of(:monitoring_tool).is_at_most(100) }
describe 'fingerprint' do
let_it_be(:fingerprint) { 'fingerprint' }
let_it_be(:existing_alert) { create(:alert_management_alert, fingerprint: fingerprint) }
let(:new_alert) { build(:alert_management_alert, fingerprint: fingerprint, project: project) }
subject { new_alert }
context 'adding an alert with the same fingerprint' do
context 'same project' do
let(:project) { existing_alert.project }
it { is_expected.not_to be_valid }
end
context 'different project' do
let(:project) { create(:project) }
it { is_expected.to be_valid }
end
end
end
describe 'hosts' do
subject(:alert) { build(:alert_management_alert, hosts: hosts) }
context 'over 255 total chars' do
let(:hosts) { ['111.111.111.111'] * 18 }
it { is_expected.not_to be_valid }
end
context 'under 255 chars' do
let(:hosts) { ['111.111.111.111'] * 17 }
it { is_expected.to be_valid }
end
end
end
describe 'enums' do
let(:severity_values) do
{ critical: 0, high: 1, medium: 2, low: 3, info: 4, unknown: 5 }
end
let(:status_values) do
{ triggered: 0, acknowledged: 1, resolved: 2, ignored: 3 }
end
it { is_expected.to define_enum_for(:severity).with_values(severity_values) }
it { is_expected.to define_enum_for(:status).with_values(status_values) }
end
describe 'fingerprint setter' do
let(:alert) { build(:alert_management_alert) }
subject(:set_fingerprint) { alert.fingerprint = fingerprint }
let(:fingerprint) { 'test' }
it 'sets to the SHA1 of the value' do
expect { set_fingerprint }
.to change { alert.fingerprint }
.from(nil)
.to(Digest::SHA1.hexdigest(fingerprint))
end
describe 'testing length of 40' do
where(:input) do
[
'test',
'another test',
'a' * 1000,
12345
]
end
with_them do
let(:fingerprint) { input }
it 'sets the fingerprint to 40 chars' do
set_fingerprint
expect(alert.fingerprint.size).to eq(40)
end
end
end
context 'blank value given' do
let(:fingerprint) { '' }
it 'does not set the fingerprint' do
expect { set_fingerprint }
.not_to change { alert.fingerprint }
.from(nil)
end
end
end
end
......@@ -14,6 +14,7 @@ describe Issue do
it { is_expected.to have_many(:assignees) }
it { is_expected.to have_many(:user_mentions).class_name("IssueUserMention") }
it { is_expected.to have_one(:sentry_issue) }
it { is_expected.to have_one(:alert_management_alert) }
end
describe 'modules' do
......
......@@ -110,6 +110,7 @@ describe Project do
it { is_expected.to have_many(:source_pipelines) }
it { is_expected.to have_many(:prometheus_alert_events) }
it { is_expected.to have_many(:self_managed_prometheus_alert_events) }
it { is_expected.to have_many(:alert_management_alerts) }
it { is_expected.to have_many(:jira_imports) }
it_behaves_like 'model with repository' do
......
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