diff --git a/app/models/clusters/cluster_enabled_grant.rb b/app/models/clusters/cluster_enabled_grant.rb new file mode 100644 index 0000000000000000000000000000000000000000..4dca6a78759b9486b248d07cf661b73f36131c53 --- /dev/null +++ b/app/models/clusters/cluster_enabled_grant.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Clusters + class ClusterEnabledGrant < ApplicationRecord + self.table_name = 'cluster_enabled_grants' + + belongs_to :namespace + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index af29850971f9bfcad88854f1bcb4c320c531ff78..6234d1fa6825fab69e514e1279fb3a73f6a6223d 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -73,6 +73,8 @@ class Namespace < ApplicationRecord has_one :ci_namespace_mirror, class_name: 'Ci::NamespaceMirror' has_many :sync_events, class_name: 'Namespaces::SyncEvent' + has_one :cluster_enabled_grant, inverse_of: :namespace, class_name: 'Clusters::ClusterEnabledGrant' + validates :owner, presence: true, if: ->(n) { n.owner_required? } validates :name, presence: true, @@ -530,13 +532,19 @@ def storage_enforcement_date end def certificate_based_clusters_enabled? - ::Gitlab::SafeRequestStore.fetch("certificate_based_clusters:ns:#{self.id}") do - Feature.enabled?(:certificate_based_clusters, self, type: :ops) - end + cluster_enabled_granted? || certificate_based_clusters_enabled_ff? end private + def cluster_enabled_granted? + root_ancestor.cluster_enabled_grant.present? && (Gitlab.com? || Gitlab.dev_or_test_env?) + end + + def certificate_based_clusters_enabled_ff? + Feature.enabled?(:certificate_based_clusters, type: :ops) + end + def expire_child_caches Namespace.where(id: descendants).each_batch do |namespaces| namespaces.touch_all diff --git a/db/docs/cluster_enabled_grants.yml b/db/docs/cluster_enabled_grants.yml new file mode 100644 index 0000000000000000000000000000000000000000..7a8faba26d69ed7d5c738d665ac4f8794ec9353c --- /dev/null +++ b/db/docs/cluster_enabled_grants.yml @@ -0,0 +1,9 @@ +--- +table_name: cluster_enabled_grants +classes: +- Clusters::ClusterEnabledGrant +feature_categories: +- kubernetes_management +description: Persists information about namespaces which got an extended life for certificate based clusters +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87149 +milestone: '15.1' diff --git a/db/migrate/20220519013213_create_cluster_enabled_grants.rb b/db/migrate/20220519013213_create_cluster_enabled_grants.rb new file mode 100644 index 0000000000000000000000000000000000000000..45c18ecca45112ed2e2f57eeeaaa05f27706d5f3 --- /dev/null +++ b/db/migrate/20220519013213_create_cluster_enabled_grants.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateClusterEnabledGrants < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + def change + create_table :cluster_enabled_grants do |t| + t.references :namespace, index: { unique: true }, null: false, foreign_key: { on_delete: :cascade } + t.datetime_with_timezone :created_at, null: false + end + end +end diff --git a/db/post_migrate/20220519045133_bulk_insert_cluster_enabled_grants.rb b/db/post_migrate/20220519045133_bulk_insert_cluster_enabled_grants.rb new file mode 100644 index 0000000000000000000000000000000000000000..6c1d90586738aadb54d938d3963b29624bd5171d --- /dev/null +++ b/db/post_migrate/20220519045133_bulk_insert_cluster_enabled_grants.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class BulkInsertClusterEnabledGrants < Gitlab::Database::Migration[2.0] + restrict_gitlab_migration gitlab_schema: :gitlab_main + + disable_ddl_transaction! + + def up + return unless Gitlab.dev_or_test_env? || Gitlab.com? + + define_batchable_model('cluster_groups').each_batch do |batch| + min, max = batch.pick('MIN(id), MAX(id)') + + bulk_insert = <<-SQL + INSERT INTO cluster_enabled_grants (namespace_id, created_at) + SELECT DISTINCT(traversal_ids[1]), NOW() + FROM cluster_groups + INNER JOIN namespaces ON cluster_groups.group_id = namespaces.id + WHERE cluster_groups.id BETWEEN #{min} AND #{max} + ON CONFLICT (namespace_id) DO NOTHING + SQL + + connection.execute(bulk_insert) + end + + define_batchable_model('cluster_projects').each_batch do |batch| + min, max = batch.pick('MIN(id), MAX(id)') + + bulk_insert = <<-SQL + INSERT INTO cluster_enabled_grants (namespace_id, created_at) + SELECT DISTINCT(traversal_ids[1]), NOW() + FROM cluster_projects + INNER JOIN projects ON cluster_projects.project_id = projects.id + INNER JOIN namespaces on projects.namespace_id = namespaces.id + WHERE cluster_projects.id BETWEEN #{min} AND #{max} + ON CONFLICT (namespace_id) DO NOTHING + SQL + + connection.execute(bulk_insert) + end + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20220519013213 b/db/schema_migrations/20220519013213 new file mode 100644 index 0000000000000000000000000000000000000000..c3575b668e4e0b5328609b8638554be09a5e1cd6 --- /dev/null +++ b/db/schema_migrations/20220519013213 @@ -0,0 +1 @@ +d8ae65034a7768c238a65c4c36d709364dee65652da93c368774e3828b0edb41 \ No newline at end of file diff --git a/db/schema_migrations/20220519045133 b/db/schema_migrations/20220519045133 new file mode 100644 index 0000000000000000000000000000000000000000..099a74f8b34894d4403bb34af608a2129465fac4 --- /dev/null +++ b/db/schema_migrations/20220519045133 @@ -0,0 +1 @@ +99fd05c3102300c115edf09a54feddfd9721bf63ae09063e6dc9d568be6d8f1f \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index db235d6d2d6994610f6c4335d9b65582a756c296..26897ba471f34e5462a8c21823927c49a0e8ddae 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13341,6 +13341,21 @@ CREATE SEQUENCE cluster_agents_id_seq ALTER SEQUENCE cluster_agents_id_seq OWNED BY cluster_agents.id; +CREATE TABLE cluster_enabled_grants ( + id bigint NOT NULL, + namespace_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE cluster_enabled_grants_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE cluster_enabled_grants_id_seq OWNED BY cluster_enabled_grants.id; + CREATE TABLE cluster_groups ( id integer NOT NULL, cluster_id integer NOT NULL, @@ -22607,6 +22622,8 @@ ALTER TABLE ONLY cluster_agent_tokens ALTER COLUMN id SET DEFAULT nextval('clust ALTER TABLE ONLY cluster_agents ALTER COLUMN id SET DEFAULT nextval('cluster_agents_id_seq'::regclass); +ALTER TABLE ONLY cluster_enabled_grants ALTER COLUMN id SET DEFAULT nextval('cluster_enabled_grants_id_seq'::regclass); + ALTER TABLE ONLY cluster_groups ALTER COLUMN id SET DEFAULT nextval('cluster_groups_id_seq'::regclass); ALTER TABLE ONLY cluster_platforms_kubernetes ALTER COLUMN id SET DEFAULT nextval('cluster_platforms_kubernetes_id_seq'::regclass); @@ -24331,6 +24348,9 @@ ALTER TABLE ONLY cluster_agent_tokens ALTER TABLE ONLY cluster_agents ADD CONSTRAINT cluster_agents_pkey PRIMARY KEY (id); +ALTER TABLE ONLY cluster_enabled_grants + ADD CONSTRAINT cluster_enabled_grants_pkey PRIMARY KEY (id); + ALTER TABLE ONLY cluster_groups ADD CONSTRAINT cluster_groups_pkey PRIMARY KEY (id); @@ -27392,6 +27412,8 @@ CREATE INDEX index_cluster_agents_on_created_by_user_id ON cluster_agents USING CREATE UNIQUE INDEX index_cluster_agents_on_project_id_and_name ON cluster_agents USING btree (project_id, name); +CREATE UNIQUE INDEX index_cluster_enabled_grants_on_namespace_id ON cluster_enabled_grants USING btree (namespace_id); + CREATE UNIQUE INDEX index_cluster_groups_on_cluster_id_and_group_id ON cluster_groups USING btree (cluster_id, group_id); CREATE INDEX index_cluster_groups_on_group_id ON cluster_groups USING btree (group_id); @@ -32843,6 +32865,9 @@ ALTER TABLE ONLY approval_merge_request_rules_users ALTER TABLE ONLY required_code_owners_sections ADD CONSTRAINT fk_rails_817708cf2d FOREIGN KEY (protected_branch_id) REFERENCES protected_branches(id) ON DELETE CASCADE; +ALTER TABLE ONLY cluster_enabled_grants + ADD CONSTRAINT fk_rails_8336ce35af FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY dast_site_profiles ADD CONSTRAINT fk_rails_83e309d69e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml index d779da6e597f732207890604e7d355a48368a021..7ba6249b5983b349d6d4d3b0a69f7828fd382386 100644 --- a/lib/gitlab/database/gitlab_schemas.yml +++ b/lib/gitlab/database/gitlab_schemas.yml @@ -121,6 +121,7 @@ ci_unit_tests: :gitlab_ci ci_variables: :gitlab_ci cluster_agents: :gitlab_main cluster_agent_tokens: :gitlab_main +cluster_enabled_grants: :gitlab_main cluster_groups: :gitlab_main cluster_platforms_kubernetes: :gitlab_main cluster_projects: :gitlab_main diff --git a/spec/factories/clusters/cluster_enabled_grant.rb b/spec/factories/clusters/cluster_enabled_grant.rb new file mode 100644 index 0000000000000000000000000000000000000000..f995bc876f3e16fb7a0f3bc89f453b15bf1d0e0c --- /dev/null +++ b/spec/factories/clusters/cluster_enabled_grant.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :cluster_enabled_grant, class: 'Clusters::ClusterEnabledGrant' do + namespace + end +end diff --git a/spec/migrations/bulk_insert_cluster_enabled_grants_spec.rb b/spec/migrations/bulk_insert_cluster_enabled_grants_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a359a78ab45b55075227d5dd1d5e365c9aba467b --- /dev/null +++ b/spec/migrations/bulk_insert_cluster_enabled_grants_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BulkInsertClusterEnabledGrants, :migration do + let(:migration) { described_class.new } + + let(:cluster_enabled_grants) { table(:cluster_enabled_grants) } + let(:namespaces) { table(:namespaces) } + let(:cluster_projects) { table(:cluster_projects) } + let(:cluster_groups) { table(:cluster_groups) } + let(:clusters) { table(:clusters) } + let(:projects) { table(:projects) } + + context 'with namespaces, cluster_groups and cluster_projects' do + it 'creates unique cluster_enabled_grants for root_namespaces with clusters' do + # Does not create grants for namespaces without clusters + namespaces.create!(id: 1, path: 'eee', name: 'eee', traversal_ids: [1]) # not used + + # Creates unique grant for a root namespace with its own cluster + root_ns_with_own_cluster = namespaces.create!(id: 2, path: 'ddd', name: 'ddd', traversal_ids: [2]) + cluster_root_ns_with_own_cluster = clusters.create!(name: 'cluster_root_ns_with_own_cluster') + cluster_groups.create!( + cluster_id: cluster_root_ns_with_own_cluster.id, + group_id: root_ns_with_own_cluster.id) + + # Creates unique grant for namespaces with multiple sub-group clusters + root_ns_with_sub_group_clusters = namespaces.create!(id: 3, path: 'aaa', name: 'aaa', traversal_ids: [3]) + + subgroup_1 = namespaces.create!( + id: 4, + path: 'bbb', + name: 'bbb', + parent_id: root_ns_with_sub_group_clusters.id, + traversal_ids: [root_ns_with_sub_group_clusters.id, 4]) + cluster_subgroup_1 = clusters.create!(name: 'cluster_subgroup_1') + cluster_groups.create!(cluster_id: cluster_subgroup_1.id, group_id: subgroup_1.id) + + subgroup_2 = namespaces.create!( + id: 5, + path: 'ccc', + name: 'ccc', + parent_id: subgroup_1.id, + traversal_ids: [root_ns_with_sub_group_clusters.id, subgroup_1.id, 5]) + cluster_subgroup_2 = clusters.create!(name: 'cluster_subgroup_2') + cluster_groups.create!(cluster_id: cluster_subgroup_2.id, group_id: subgroup_2.id) + + # Creates unique grant for a root namespace with multiple projects clusters + root_ns_with_project_group_clusters = namespaces.create!(id: 6, path: 'fff', name: 'fff', traversal_ids: [6]) + + project_namespace_1 = namespaces.create!(id: 7, path: 'ggg', name: 'ggg', traversal_ids: [7]) + project_1 = projects.create!( + name: 'project_1', + namespace_id: root_ns_with_project_group_clusters.id, + project_namespace_id: project_namespace_1.id) + cluster_project_1 = clusters.create!(name: 'cluster_project_1') + cluster_projects.create!(cluster_id: cluster_project_1.id, project_id: project_1.id) + + project_namespace_2 = namespaces.create!(id: 8, path: 'hhh', name: 'hhh', traversal_ids: [8]) + project_2 = projects.create!( + name: 'project_2', + namespace_id: root_ns_with_project_group_clusters.id, + project_namespace_id: project_namespace_2.id) + cluster_project_2 = clusters.create!(name: 'cluster_project_2') + cluster_projects.create!(cluster_id: cluster_project_2.id, project_id: project_2.id) + + migrate! + + expected_cluster_enabled_grants = [ + root_ns_with_sub_group_clusters.id, + root_ns_with_own_cluster.id, + root_ns_with_project_group_clusters.id + ] + + expect(cluster_enabled_grants.pluck(:namespace_id)).to match_array(expected_cluster_enabled_grants) + end + end + + context 'without namespaces, cluster_groups or cluster_projects' do + it 'does nothing' do + expect { migrate! }.not_to change { cluster_enabled_grants.count } + end + end +end diff --git a/spec/models/clusters/cluster_enabled_grant_spec.rb b/spec/models/clusters/cluster_enabled_grant_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1418d854b41c1627ff91abcb275a44ab14d9bc1e --- /dev/null +++ b/spec/models/clusters/cluster_enabled_grant_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Clusters::ClusterEnabledGrant do + it { is_expected.to belong_to :namespace } +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 4373d9a0b24ee48e2f3c2b51d4e23f0c522607bf..02d2c9a88affe56c5e628736aaafe967b9f3527e 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -31,6 +31,7 @@ it { is_expected.to have_many :pending_builds } it { is_expected.to have_one :namespace_route } it { is_expected.to have_many :namespace_members } + it { is_expected.to have_one :cluster_enabled_grant } it do is_expected.to have_one(:ci_cd_settings).class_name('NamespaceCiCdSetting').inverse_of(:namespace).autosave(true) @@ -2251,27 +2252,23 @@ def project_rugged(project) end describe '#certificate_based_clusters_enabled?' do - it 'does not call Feature.enabled? twice with request_store', :request_store do - expect(Feature).to receive(:enabled?).once - - namespace.certificate_based_clusters_enabled? - namespace.certificate_based_clusters_enabled? - end - - it 'call Feature.enabled? twice without request_store' do - expect(Feature).to receive(:enabled?).twice - - namespace.certificate_based_clusters_enabled? - namespace.certificate_based_clusters_enabled? - end - context 'with ff disabled' do before do stub_feature_flags(certificate_based_clusters: false) end - it 'is truthy' do - expect(namespace.certificate_based_clusters_enabled?).to be_falsy + context 'with a cluster_enabled_grant' do + it 'is truthy' do + create(:cluster_enabled_grant, namespace: namespace) + + expect(namespace.certificate_based_clusters_enabled?).to be_truthy + end + end + + context 'without a cluster_enabled_grant' do + it 'is falsy' do + expect(namespace.certificate_based_clusters_enabled?).to be_falsy + end end end @@ -2280,8 +2277,18 @@ def project_rugged(project) stub_feature_flags(certificate_based_clusters: true) end - it 'is truthy' do - expect(namespace.certificate_based_clusters_enabled?).to be_truthy + context 'with a cluster_enabled_grant' do + it 'is truthy' do + create(:cluster_enabled_grant, namespace: namespace) + + expect(namespace.certificate_based_clusters_enabled?).to be_truthy + end + end + + context 'without a cluster_enabled_grant' do + it 'is truthy' do + expect(namespace.certificate_based_clusters_enabled?).to be_truthy + end end end end