Skip to content
Snippets Groups Projects
Commit fe92aec1 authored by Tao Guo's avatar Tao Guo Committed by Sean Arnold
Browse files

Create a new table for storing automation rules

As part of the No-code Automation feature, this table is used to store
the automation rules and their subscription to the domain events.

Changelog: added
parent 21b301e8
No related branches found
No related tags found
3 merge requests!112033Draft: Port snapshot serializer,!112030Draft: Port snpshot serializer,!111021Add automation rule model and dispatch service
Showing
with 322 additions and 0 deletions
......@@ -67,6 +67,8 @@
- 2
- - auto_merge
- 3
- - automation_execute_rule
- 1
- - background_migration
- 1
- - background_migration_ci_database
......
---
table_name: automation_rules
description: Stores automation rules and their trigger events
classes:
- Automation::Rule
feature_categories:
- no_code_automation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111021
milestone: '15.9'
gitlab_schema: gitlab_main
# frozen_string_literal: true
class CreateAutomationRules < Gitlab::Database::Migration[2.1]
enable_lock_retries!
def up
create_table :automation_rules do |t|
t.references :namespace, null: false, index: false, foreign_key: { on_delete: :cascade }
t.boolean :issues_events, default: false, null: false
t.boolean :merge_requests_events, default: false, null: false
t.boolean :permanently_disabled, default: false, null: false
t.text :name, null: false, limit: 255
t.text :rule, null: false, limit: 2048
t.timestamps_with_timezone null: false
t.index 'namespace_id, LOWER(name)',
name: 'index_automation_rules_namespace_id_name',
unique: true
t.index [:namespace_id, :permanently_disabled],
name: 'index_automation_rules_namespace_id_permanently_disabled'
end
end
def down
drop_table :automation_rules
end
end
51ce125f058811cd0f118429049389d9b67479628472830bce4c04cc81969a37
\ No newline at end of file
......@@ -12069,6 +12069,29 @@ CREATE SEQUENCE authentication_events_id_seq
 
ALTER SEQUENCE authentication_events_id_seq OWNED BY authentication_events.id;
 
CREATE TABLE automation_rules (
id bigint NOT NULL,
namespace_id bigint NOT NULL,
issues_events boolean DEFAULT false NOT NULL,
merge_requests_events boolean DEFAULT false NOT NULL,
permanently_disabled boolean DEFAULT false NOT NULL,
name text NOT NULL,
rule text NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
CONSTRAINT check_0be3e2c953 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_ed5a4fcbd5 CHECK ((char_length(rule) <= 2048))
);
CREATE SEQUENCE automation_rules_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE automation_rules_id_seq OWNED BY automation_rules.id;
CREATE TABLE award_emoji (
id integer NOT NULL,
name character varying,
......@@ -24096,6 +24119,8 @@ ALTER TABLE ONLY audit_events_streaming_headers ALTER COLUMN id SET DEFAULT next
 
ALTER TABLE ONLY authentication_events ALTER COLUMN id SET DEFAULT nextval('authentication_events_id_seq'::regclass);
 
ALTER TABLE ONLY automation_rules ALTER COLUMN id SET DEFAULT nextval('automation_rules_id_seq'::regclass);
ALTER TABLE ONLY award_emoji ALTER COLUMN id SET DEFAULT nextval('award_emoji_id_seq'::regclass);
 
ALTER TABLE ONLY background_migration_jobs ALTER COLUMN id SET DEFAULT nextval('background_migration_jobs_id_seq'::regclass);
......@@ -25819,6 +25844,9 @@ ALTER TABLE ONLY audit_events_streaming_headers
ALTER TABLE ONLY authentication_events
ADD CONSTRAINT authentication_events_pkey PRIMARY KEY (id);
 
ALTER TABLE ONLY automation_rules
ADD CONSTRAINT automation_rules_pkey PRIMARY KEY (id);
ALTER TABLE ONLY award_emoji
ADD CONSTRAINT award_emoji_pkey PRIMARY KEY (id);
 
......@@ -28921,6 +28949,10 @@ CREATE INDEX index_authentication_events_on_provider ON authentication_events US
 
CREATE INDEX index_authentication_events_on_user_and_ip_address_and_result ON authentication_events USING btree (user_id, ip_address, result);
 
CREATE UNIQUE INDEX index_automation_rules_namespace_id_name ON automation_rules USING btree (namespace_id, lower(name));
CREATE INDEX index_automation_rules_namespace_id_permanently_disabled ON automation_rules USING btree (namespace_id, permanently_disabled);
CREATE INDEX index_award_emoji_on_awardable_type_and_awardable_id ON award_emoji USING btree (awardable_type, awardable_id);
 
CREATE UNIQUE INDEX index_aws_roles_on_role_external_id ON aws_roles USING btree (role_external_id);
......@@ -34547,6 +34579,9 @@ ALTER TABLE ONLY approval_merge_request_rules
ALTER TABLE ONLY namespace_statistics
ADD CONSTRAINT fk_rails_0062050394 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
 
ALTER TABLE ONLY automation_rules
ADD CONSTRAINT fk_rails_025b519b8d FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY incident_management_oncall_participants
ADD CONSTRAINT fk_rails_032b12996a FOREIGN KEY (oncall_rotation_id) REFERENCES incident_management_oncall_rotations(id) ON DELETE CASCADE;
 
# frozen_string_literal: true
module Automation
class Rule < ApplicationRecord
include TriggerableHooks
include StripAttribute
FAILURE_THRESHOLD = 3
EXCEEDED_FAILURE_THRESHOLD = FAILURE_THRESHOLD + 1
self.table_name = 'automation_rules'
triggerable_hooks [
:issue_hooks,
:merge_request_hooks
]
scope :executable, -> { where(permanently_disabled: false) }
belongs_to :namespace, inverse_of: :automation_rules, optional: false
strip_attributes! :name
validates :name,
presence: true,
length: { maximum: 255 },
uniqueness: { case_sensitive: false, scope: [:namespace_id] }
end
end
......@@ -33,6 +33,8 @@ module Namespace
has_many :ci_minutes_additional_packs, class_name: "Ci::Minutes::AdditionalPack"
has_many :automation_rules, class_name: '::Automation::Rule'
has_one :security_orchestration_policy_configuration,
class_name: 'Security::OrchestrationPolicyConfiguration',
foreign_key: :namespace_id,
......
......@@ -575,6 +575,17 @@ def execute_external_compliance_hooks(data)
end
end
override :execute_hooks
def execute_hooks(data, hooks_scope = :push_hooks)
super
run_after_commit_or_now do
if ::Feature.enabled?(:no_code_automation_mvc, self) && licensed_feature_available?(:no_code_automation)
Automation::DispatchService.new(container: project_namespace).execute(data, hooks_scope)
end
end
end
override :triggered_hooks
def triggered_hooks(scope, data)
triggered = super
......
# frozen_string_literal: true
module Automation
class DispatchService < ::BaseContainerService
def execute(data, hook)
container.automation_rules.hooks_for(hook).each do |rule|
Automation::ExecuteRuleWorker.perform_async(rule.id)
end
end
end
end
......@@ -1056,6 +1056,15 @@
:weight: 1
:idempotent: true
:tags: []
- :name: automation_execute_rule
:worker_name: Automation::ExecuteRuleWorker
:feature_category: :no_code_automation
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: ci_batch_reset_minutes
:worker_name: Ci::BatchResetMinutesWorker
:feature_category: :continuous_integration
......
# frozen_string_literal: true
module Automation
class ExecuteRuleWorker
include ApplicationWorker
feature_category :no_code_automation
data_consistency :always
idempotent!
def perform(rule_id)
Gitlab::AppLogger.info 'Placeholder for performing automation rules'
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :automation_rule, class: 'Automation::Rule' do
namespace
name { generate(:name) }
rule { '-' }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Automation::Rule, type: :model, feature_category: :no_code_automation do
describe 'associations' do
it { is_expected.to belong_to(:namespace).required }
end
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(TriggerableHooks) }
it { is_expected.to include_module(StripAttribute) }
end
describe 'validations' do
subject { build(:automation_rule) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).case_insensitive.scoped_to([:namespace_id]) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
end
describe 'scopes' do
describe 'executable' do
subject { described_class.executable }
let_it_be(:disabled_rule) { create(:automation_rule, permanently_disabled: true) }
let_it_be(:executable_rule) { create(:automation_rule, permanently_disabled: false) }
it { is_expected.to contain_exactly(executable_rule) }
end
end
describe '#name' do
it 'strips name' do
rule = described_class.new(name: ' Rule123 ')
rule.valid?
expect(rule.name).to eq('Rule123')
end
end
describe 'triggerable hooks' do
let_it_be_with_reload(:namespace) { create(:project_namespace) }
{
issue_hooks: :issues_events,
merge_request_hooks: :merge_requests_events
}.each do |scope, trigger_event|
it "returns rules based on the #{scope} scope" do
rule = create(:automation_rule, namespace: namespace, trigger_event.to_sym => true)
expect(described_class.hooks_for(scope)).to contain_exactly(rule)
end
end
end
end
......@@ -1142,6 +1142,49 @@
end
describe "#execute_hooks" do
context 'dispatch automation runs' do
let(:project) { build(:project) }
subject(:service) { instance_double('Automation::DispatchService') }
before do
stub_licensed_features(no_code_automation: feature_enabled)
stub_feature_flags(no_code_automation_mvc: flag_enabled)
allow(Automation::DispatchService)
.to receive(:new)
.with(container: project.project_namespace)
.and_return(service)
allow(service).to receive(:execute)
project.execute_hooks({ some: 'info' }, :issue_hooks)
end
context 'when both no_code_automation feature and no_code_automation_mvc flag are enabled' do
let(:feature_enabled) { true }
let(:flag_enabled) { true }
it 'dispatches hook data to Automation::DispatchService' do
expect(service).to have_received(:execute).with({ some: 'info' }, :issue_hooks)
end
end
context 'when no_code_automation feature is disabled' do
let(:feature_enabled) { false }
let(:flag_enabled) { true }
it { is_expected.not_to have_received(:execute) }
end
context 'when no_code_automation_mvc flag is disabled' do
let(:flag_enabled) { false }
let(:feature_enabled) { true }
it { is_expected.not_to have_received(:execute) }
end
end
context "group hooks" do
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Automation::DispatchService, feature_category: :no_code_automation do
describe '#execute' do
let_it_be(:namespace) { create(:project_namespace) }
let_it_be(:issue_rule) { create(:automation_rule, namespace: namespace, issues_events: true) }
let_it_be(:mr_rule) { create(:automation_rule, namespace: namespace, merge_requests_events: true) }
let(:data) { { info: '123' } }
before do
allow(Automation::ExecuteRuleWorker).to receive(:perform_async)
described_class.new(container: namespace).execute(data, hook)
end
describe 'execute' do
context 'when dispatching issue_hooks' do
let(:hook) { :issue_hooks }
it 'performs predefined issue rule' do
expect(Automation::ExecuteRuleWorker).to have_received(:perform_async)
.with(issue_rule.id)
end
end
context 'when dispatching merge_request_hooks' do
let(:hook) { :merge_request_hooks }
it 'performs predefined issue rule' do
expect(Automation::ExecuteRuleWorker).to have_received(:perform_async)
.with(mr_rule.id)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Automation::ExecuteRuleWorker, feature_category: :no_code_automation do
let(:worker) { described_class.new }
let(:rule_id) { 1 }
describe '#perform' do
it 'logs placeholder message for now' do
expect(Gitlab::AppLogger).to receive(:info)
.with('Placeholder for performing automation rules')
worker.perform(rule_id)
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment