Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb')
-rw-r--r--spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb561
1 files changed, 561 insertions, 0 deletions
diff --git a/spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb b/spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb
new file mode 100644
index 00000000000..ad9a3a6e257
--- /dev/null
+++ b/spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb
@@ -0,0 +1,561 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, query_analyzers: false, stub_feature_flags: false do
+ let(:schema_class) { Class.new(Gitlab::Database::Migration[1.0]).include(described_class) }
+
+ describe '#restrict_gitlab_migration' do
+ it 'invalid schema raises exception' do
+ expect { schema_class.restrict_gitlab_migration gitlab_schema: :gitlab_non_exisiting }
+ .to raise_error /Unknown 'gitlab_schema:/
+ end
+
+ it 'does configure allowed_gitlab_schema' do
+ schema_class.restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ expect(schema_class.allowed_gitlab_schemas).to eq(%i[gitlab_main])
+ end
+ end
+
+ context 'when executing migrations' do
+ using RSpec::Parameterized::TableSyntax
+
+ where do
+ {
+ "does create table in gitlab_main and gitlab_ci" => {
+ migration: ->(klass) do
+ def change
+ create_table :_test_table do |t|
+ t.references :project, foreign_key: true, null: false
+ t.timestamps_with_timezone null: false
+ end
+ end
+ end,
+ query_matcher: /CREATE TABLE "_test_table"/,
+ expected: {
+ no_gitlab_schema: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :ddl_not_allowed,
+ ci: :ddl_not_allowed
+ },
+ gitlab_schema_gitlab_main: {
+ main: :ddl_not_allowed,
+ ci: :skipped
+ }
+ }
+ },
+ "does add column to projects in gitlab_main and gitlab_ci" => {
+ migration: ->(klass) do
+ def change
+ add_column :projects, :__test_column, :integer
+ end
+ end,
+ query_matcher: /ALTER TABLE "projects" ADD "__test_column" integer/,
+ expected: {
+ no_gitlab_schema: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :ddl_not_allowed,
+ ci: :ddl_not_allowed
+ },
+ gitlab_schema_gitlab_main: {
+ main: :ddl_not_allowed,
+ ci: :skipped
+ }
+ }
+ },
+ "does add column to ci_builds in gitlab_main and gitlab_ci" => {
+ migration: ->(klass) do
+ def change
+ add_column :ci_builds, :__test_column, :integer
+ end
+ end,
+ query_matcher: /ALTER TABLE "ci_builds" ADD "__test_column" integer/,
+ expected: {
+ no_gitlab_schema: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :ddl_not_allowed,
+ ci: :ddl_not_allowed
+ },
+ gitlab_schema_gitlab_main: {
+ main: :ddl_not_allowed,
+ ci: :skipped
+ }
+ }
+ },
+ "does add index to projects in gitlab_main and gitlab_ci" => {
+ migration: ->(klass) do
+ def change
+ # Due to running in transactin we cannot use `add_concurrent_index`
+ add_index :projects, :hidden
+ end
+ end,
+ query_matcher: /CREATE INDEX/,
+ expected: {
+ no_gitlab_schema: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :ddl_not_allowed,
+ ci: :ddl_not_allowed
+ },
+ gitlab_schema_gitlab_main: {
+ main: :ddl_not_allowed,
+ ci: :skipped
+ }
+ }
+ },
+ "does add index to ci_builds in gitlab_main and gitlab_ci" => {
+ migration: ->(klass) do
+ def change
+ # Due to running in transactin we cannot use `add_concurrent_index`
+ add_index :ci_builds, :tag, where: "type = 'Ci::Build'", name: 'index_ci_builds_on_tag_and_type_eq_ci_build'
+ end
+ end,
+ query_matcher: /CREATE INDEX/,
+ expected: {
+ no_gitlab_schema: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :ddl_not_allowed,
+ ci: :ddl_not_allowed
+ },
+ gitlab_schema_gitlab_main: {
+ main: :ddl_not_allowed,
+ ci: :skipped
+ }
+ }
+ },
+ "does create trigger in gitlab_main and gitlab_ci" => {
+ migration: ->(klass) do
+ include Gitlab::Database::SchemaHelpers
+
+ def up
+ create_trigger_function('_test_trigger_function', replace: true) do
+ <<~SQL
+ RETURN NULL;
+ SQL
+ end
+ end
+
+ def down
+ drop_function('_test_trigger_function')
+ end
+ end,
+ query_matcher: /CREATE OR REPLACE FUNCTION/,
+ expected: {
+ no_gitlab_schema: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :ddl_not_allowed,
+ ci: :ddl_not_allowed
+ },
+ gitlab_schema_gitlab_main: {
+ main: :ddl_not_allowed,
+ ci: :skipped
+ }
+ }
+ },
+ "does create schema in gitlab_main and gitlab_ci" => {
+ migration: ->(klass) do
+ include Gitlab::Database::SchemaHelpers
+
+ def up
+ execute("create schema __test_schema")
+ end
+
+ def down
+ end
+ end,
+ query_matcher: /create schema __test_schema/,
+ expected: {
+ no_gitlab_schema: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_main: {
+ # This is not properly detected today since there are no helpers
+ # available to consider this as a DDL type of change
+ main: :success,
+ ci: :skipped
+ }
+ }
+ },
+ "does attach loose foreign key trigger in gitlab_main and gitlab_ci" => {
+ migration: ->(klass) do
+ include Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers
+
+ enable_lock_retries!
+
+ def up
+ track_record_deletions(:audit_events)
+ end
+
+ def down
+ untrack_record_deletions(:audit_events)
+ end
+ end,
+ query_matcher: /CREATE TRIGGER/,
+ expected: {
+ no_gitlab_schema: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :ddl_not_allowed,
+ ci: :ddl_not_allowed
+ },
+ gitlab_schema_gitlab_main: {
+ main: :ddl_not_allowed,
+ ci: :skipped
+ }
+ }
+ },
+ "does insert into software_licenses" => {
+ migration: ->(klass) do
+ def up
+ software_license_class.create!(name: 'aaa')
+ end
+
+ def down
+ software_license_class.where(name: 'aaa').delete_all
+ end
+
+ def software_license_class
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'software_licenses'
+ end
+ end
+ end,
+ query_matcher: /INSERT INTO "software_licenses"/,
+ expected: {
+ no_gitlab_schema: {
+ main: :dml_not_allowed,
+ ci: :dml_not_allowed
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :dml_access_denied,
+ ci: :dml_access_denied
+ },
+ gitlab_schema_gitlab_main: {
+ main: :success,
+ ci: :skipped
+ }
+ }
+ },
+ "does raise exception when accessing tables outside of gitlab_main" => {
+ migration: ->(klass) do
+ def up
+ ci_instance_variables_class.create!(variable_type: 1, key: 'aaa')
+ end
+
+ def down
+ ci_instance_variables_class.delete_all
+ end
+
+ def ci_instance_variables_class
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'ci_instance_variables'
+ end
+ end
+ end,
+ query_matcher: /INSERT INTO "ci_instance_variables"/,
+ expected: {
+ no_gitlab_schema: {
+ main: :dml_not_allowed,
+ ci: :dml_not_allowed
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :dml_access_denied,
+ ci: :dml_access_denied
+ },
+ gitlab_schema_gitlab_main: {
+ main: :dml_access_denied,
+ ci: :skipped
+ }
+ }
+ },
+ "does allow modifying gitlab_shared" => {
+ migration: ->(klass) do
+ def up
+ detached_partitions_class.create!(drop_after: Time.current, table_name: '_test_table')
+ end
+
+ def down
+ end
+
+ def detached_partitions_class
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'detached_partitions'
+ end
+ end
+ end,
+ query_matcher: /INSERT INTO "detached_partitions"/,
+ expected: {
+ no_gitlab_schema: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_main: {
+ # TBD: This allow to selectively modify shared tables in context of a specific DB only
+ main: :success,
+ ci: :skipped
+ }
+ }
+ },
+ "does update data in batches of gitlab_main, but skips gitlab_ci" => {
+ migration: ->(klass) do
+ def up
+ update_column_in_batches(:projects, :archived, true) do |table, query|
+ query.where(table[:archived].eq(false)) # rubocop:disable CodeReuse/ActiveRecord
+ end
+ end
+
+ def down
+ # no-op
+ end
+ end,
+ query_matcher: /FROM "projects"/,
+ expected: {
+ no_gitlab_schema: {
+ main: :dml_not_allowed,
+ ci: :dml_not_allowed
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :dml_access_denied,
+ ci: :dml_access_denied
+ },
+ gitlab_schema_gitlab_main: {
+ main: :success,
+ ci: :skipped
+ }
+ }
+ },
+ "does not allow executing mixed DDL and DML migrations" => {
+ migration: ->(klass) do
+ def up
+ execute('UPDATE projects SET hidden=false')
+ add_index(:projects, :hidden, name: 'test_index')
+ end
+
+ def down
+ # no-op
+ end
+ end,
+ expected: {
+ no_gitlab_schema: {
+ main: :dml_not_allowed,
+ ci: :dml_not_allowed
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :dml_access_denied,
+ ci: :dml_access_denied
+ },
+ gitlab_schema_gitlab_main: {
+ main: :ddl_not_allowed,
+ ci: :skipped
+ }
+ }
+ },
+ "does schedule background migrations on gitlab_main" => {
+ migration: ->(klass) do
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(
+ define_batchable_model('vulnerability_occurrences'),
+ 'RemoveDuplicateVulnerabilitiesFindings',
+ 2.minutes.to_i,
+ batch_size: 5_000
+ )
+ end
+
+ def down
+ # no-op
+ end
+ end,
+ query_matcher: /FROM "vulnerability_occurrences"/,
+ expected: {
+ no_gitlab_schema: {
+ main: :dml_not_allowed,
+ ci: :dml_not_allowed
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :dml_access_denied,
+ ci: :dml_access_denied
+ },
+ gitlab_schema_gitlab_main: {
+ main: :success,
+ ci: :skipped
+ }
+ }
+ },
+ "does support prepare_async_index" => {
+ migration: ->(klass) do
+ def up
+ prepare_async_index :projects, :hidden,
+ name: :index_projects_on_hidden
+ end
+
+ def down
+ unprepare_async_index_by_name :projects, :index_projects_on_hidden
+ end
+ end,
+ query_matcher: /INSERT INTO "postgres_async_indexes"/,
+ expected: {
+ no_gitlab_schema: {
+ main: :success,
+ ci: :success
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :dml_not_allowed,
+ ci: :dml_not_allowed
+ },
+ gitlab_schema_gitlab_main: {
+ main: :dml_not_allowed,
+ ci: :skipped
+ }
+ }
+ },
+ "does raise exception when accessing current settings" => {
+ migration: ->(klass) do
+ def up
+ ApplicationSetting.last
+ end
+
+ def down
+ end
+ end,
+ query_matcher: /FROM "application_settings"/,
+ expected: {
+ no_gitlab_schema: {
+ main: :dml_not_allowed,
+ ci: :dml_not_allowed
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :dml_access_denied,
+ ci: :dml_access_denied
+ },
+ gitlab_schema_gitlab_main: {
+ main: :success,
+ ci: :skipped
+ }
+ }
+ },
+ "does raise exception when accessing feature flags" => {
+ migration: ->(klass) do
+ def up
+ Feature.enabled?(:redis_hll_tracking, type: :ops, default_enabled: :yaml)
+ end
+
+ def down
+ end
+ end,
+ query_matcher: /FROM "features"/,
+ expected: {
+ no_gitlab_schema: {
+ main: :dml_not_allowed,
+ ci: :dml_not_allowed
+ },
+ gitlab_schema_gitlab_shared: {
+ main: :dml_access_denied,
+ ci: :dml_access_denied
+ },
+ gitlab_schema_gitlab_main: {
+ main: :success,
+ ci: :skipped
+ }
+ }
+ }
+ }
+ end
+
+ with_them do
+ let(:migration_class) { Class.new(schema_class, &migration) }
+
+ Gitlab::Database.database_base_models.each do |db_config_name, model|
+ context "for db_config_name=#{db_config_name}" do
+ around do |example|
+ with_reestablished_active_record_base do
+ reconfigure_db_connection(model: ActiveRecord::Base, config_model: model)
+
+ example.run
+ end
+ end
+
+ before do
+ allow_next_instance_of(migration_class) do |migration|
+ allow(migration).to receive(:transaction_open?).and_return(false)
+ end
+ end
+
+ %i[no_gitlab_schema gitlab_schema_gitlab_main gitlab_schema_gitlab_shared].each do |restrict_gitlab_migration|
+ context "while restrict_gitlab_migration=#{restrict_gitlab_migration}" do
+ it "does run migrate :up and :down" do
+ expected_result = expected.fetch(restrict_gitlab_migration)[db_config_name.to_sym]
+ skip "not configured" unless expected_result
+
+ case restrict_gitlab_migration
+ when :no_gitlab_schema
+ # no-op
+ when :gitlab_schema_gitlab_main
+ migration_class.restrict_gitlab_migration gitlab_schema: :gitlab_main
+ when :gitlab_schema_gitlab_shared
+ migration_class.restrict_gitlab_migration gitlab_schema: :gitlab_shared
+ end
+
+ # In some cases (for :down) we ignore error and expect no other errors
+ case expected_result
+ when :success
+ expect { migration_class.migrate(:up) }.to make_queries_matching(query_matcher)
+ expect { migration_class.migrate(:down) }.not_to make_queries_matching(query_matcher)
+
+ when :dml_not_allowed
+ expect { migration_class.migrate(:up) }.to raise_error(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas::DMLNotAllowedError)
+ expect { ignore_error(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas::DMLNotAllowedError) { migration_class.migrate(:down) } }.not_to raise_error
+
+ when :dml_access_denied
+ expect { migration_class.migrate(:up) }.to raise_error(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas::DMLAccessDeniedError)
+ expect { ignore_error(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas::DMLAccessDeniedError) { migration_class.migrate(:down) } }.not_to raise_error
+
+ when :ddl_not_allowed
+ expect { migration_class.migrate(:up) }.to raise_error(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas::DDLNotAllowedError)
+ expect { ignore_error(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas::DDLNotAllowedError) { migration_class.migrate(:down) } }.not_to raise_error
+
+ when :skipped
+ expect { migration_class.migrate(:up) }.to raise_error(Gitlab::Database::MigrationHelpers::RestrictGitlabSchema::MigrationSkippedError)
+ expect { migration_class.migrate(:down) }.to raise_error(Gitlab::Database::MigrationHelpers::RestrictGitlabSchema::MigrationSkippedError)
+ end
+ end
+ end
+ end
+
+ def ignore_error(error)
+ yield
+ rescue error
+ end
+ end
+ end
+ end
+ end
+end