diff --git a/app/controllers/groups/clusters_controller.rb b/app/controllers/groups/clusters_controller.rb index 50c44b7a58b46a58ae587d193d4a8f7cbe366fff..b846fb21266e0e42a19d79ebf7733ce53500c466 100644 --- a/app/controllers/groups/clusters_controller.rb +++ b/app/controllers/groups/clusters_controller.rb @@ -3,8 +3,8 @@ class Groups::ClustersController < Clusters::ClustersController include ControllerWithCrossProjectAccessCheck - prepend_before_action :check_group_clusters_feature_flag! prepend_before_action :group + prepend_before_action :check_group_clusters_feature_flag! requires_cross_project_access layout 'group' @@ -20,6 +20,10 @@ class Groups::ClustersController < Clusters::ClustersController end def check_group_clusters_feature_flag! - render_404 unless Feature.enabled?(:group_clusters) + render_404 unless group_clusters_enabled? + end + + def group_clusters_enabled? + group.group_clusters_enabled? end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index e9b9b9b7721e05cc4fe40f668c8afac2ac397f8c..866fc555856c1574b5bf4982c27ea14f36fc79c3 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -140,7 +140,7 @@ module GroupsHelper can?(current_user, "read_group_#{resource}".to_sym, @group) end - if can?(current_user, :read_cluster, @group) && Feature.enabled?(:group_clusters) + if can?(current_user, :read_cluster, @group) && @group.group_clusters_enabled? links << :kubernetes end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 13906c903b9f03c57a7536f2410ef5d712eab58b..c9bd1728dbdf7be1ccc32b4e59f70ed4913d6a88 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -4,6 +4,7 @@ module Clusters class Cluster < ActiveRecord::Base include Presentable include Gitlab::Utils::StrongMemoize + include FromUnion self.table_name = 'clusters' @@ -86,6 +87,19 @@ module Clusters scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) } + scope :missing_kubernetes_namespace, -> (kubernetes_namespaces) do + subquery = kubernetes_namespaces.select('1').where('clusters_kubernetes_namespaces.cluster_id = clusters.id') + + where('NOT EXISTS (?)', subquery) + end + + def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc) + hierarchy_groups = clusterable.ancestors_upto(hierarchy_order: hierarchy_order).eager_load(:clusters) + hierarchy_groups = hierarchy_groups.merge(current_scope) if current_scope + + hierarchy_groups.flat_map(&:clusters) + end + def status_name if provider provider.status_name @@ -122,6 +136,16 @@ module Clusters !user? end + def all_projects + if project_type? + projects + elsif group_type? + first_group.all_projects + else + Project.none + end + end + def first_project strong_memoize(:first_project) do projects.first @@ -140,11 +164,17 @@ module Clusters platform_kubernetes.kubeclient if kubernetes? end - def find_or_initialize_kubernetes_namespace(cluster_project) - kubernetes_namespaces.find_or_initialize_by( - project: cluster_project.project, - cluster_project: cluster_project - ) + def find_or_initialize_kubernetes_namespace_for_project(project) + if project_type? + kubernetes_namespaces.find_or_initialize_by( + project: project, + cluster_project: cluster_project + ) + else + kubernetes_namespaces.find_or_initialize_by( + project: project + ) + end end def allow_user_defined_namespace? diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index e57a3383544832dbb319c5408c2daac52a2f9bd4..0107af5f8ec61d5ef5ed591668e1fb2b61c9406a 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -13,6 +13,7 @@ module DeploymentPlatform def find_deployment_platform(environment) find_cluster_platform_kubernetes(environment: environment) || + find_group_cluster_platform_kubernetes_with_feature_guard(environment: environment) || find_kubernetes_service_integration || build_cluster_and_deployment_platform end @@ -23,6 +24,18 @@ module DeploymentPlatform .last&.platform_kubernetes end + def find_group_cluster_platform_kubernetes_with_feature_guard(environment: nil) + return unless group_clusters_enabled? + + find_group_cluster_platform_kubernetes(environment: environment) + end + + # EE would override this and utilize environment argument + def find_group_cluster_platform_kubernetes(environment: nil) + Clusters::Cluster.enabled.default_environment.ancestor_clusters_for_clusterable(self) + .first&.platform_kubernetes + end + def find_kubernetes_service_integration services.deployment.reorder(nil).find_by(active: true) end diff --git a/app/models/group.rb b/app/models/group.rb index 02ddc8762afa9d346cfb0e5d971f0dca2c77d5bf..233747cc2c2ad5056298a3a1cd79665eebccfc0a 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -400,6 +400,10 @@ class Group < Namespace ensure_runners_token! end + def group_clusters_enabled? + Feature.enabled?(:group_clusters, root_ancestor, default_enabled: true) + end + private def update_two_factor_requirement diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 11b03846f0bc90c0fb22a2414ce7aed7a3f2e659..8865c164b112b906115d7ad2a96c5fe2c1d97121 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -192,9 +192,9 @@ class Namespace < ActiveRecord::Base # returns all ancestors upto but excluding the given namespace # when no namespace is given, all ancestors upto the top are returned - def ancestors_upto(top = nil) + def ancestors_upto(top = nil, hierarchy_order: nil) Gitlab::GroupHierarchy.new(self.class.where(id: id)) - .ancestors(upto: top) + .ancestors(upto: top, hierarchy_order: hierarchy_order) end def self_and_ancestors @@ -243,7 +243,7 @@ class Namespace < ActiveRecord::Base end def root_ancestor - ancestors.reorder(nil).find_by(parent_id: nil) + self_and_ancestors.reorder(nil).find_by(parent_id: nil) end def subgroup? diff --git a/app/models/project.rb b/app/models/project.rb index 0ab3ea53675af0aa9d2398131a0cde4505444e94..587bada469e87f7934cfb0e8c38ebe3ea0a55153 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -238,6 +238,7 @@ class Project < ActiveRecord::Base has_one :cluster_project, class_name: 'Clusters::Project' has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster' has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress' + has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace' has_many :prometheus_metrics @@ -300,6 +301,8 @@ class Project < ActiveRecord::Base delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team delegate :add_master, to: :team # @deprecated delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings + delegate :group_clusters_enabled?, to: :group, allow_nil: true + delegate :root_ancestor, to: :namespace, allow_nil: true # Validations validates :creator, presence: true, on: :create @@ -392,6 +395,12 @@ class Project < ActiveRecord::Base .where(project_ci_cd_settings: { group_runners_enabled: true }) end + scope :missing_kubernetes_namespace, -> (kubernetes_namespaces) do + subquery = kubernetes_namespaces.select('1').where('clusters_kubernetes_namespaces.project_id = projects.id') + + where('NOT EXISTS (?)', subquery) + end + enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } chronic_duration_attr :build_timeout_human_readable, :build_timeout, @@ -556,9 +565,9 @@ class Project < ActiveRecord::Base # returns all ancestor-groups upto but excluding the given namespace # when no namespace is given, all ancestors upto the top are returned - def ancestors_upto(top = nil) + def ancestors_upto(top = nil, hierarchy_order: nil) Gitlab::GroupHierarchy.new(Group.where(id: namespace_id)) - .base_and_ancestors(upto: top) + .base_and_ancestors(upto: top, hierarchy_order: hierarchy_order) end def lfs_enabled? @@ -1071,6 +1080,12 @@ class Project < ActiveRecord::Base path end + def all_clusters + group_clusters = Clusters::Cluster.joins(:groups).where(cluster_groups: { group_id: ancestors_upto } ) + + Clusters::Cluster.from_union([clusters, group_clusters]) + end + def items_for(entity) case entity when 'issue' then diff --git a/app/services/clusters/refresh_service.rb b/app/services/clusters/refresh_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..7c82b98a33f1415741bee2395766cf45cbab760d --- /dev/null +++ b/app/services/clusters/refresh_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Clusters + class RefreshService + def self.create_or_update_namespaces_for_cluster(cluster) + projects_with_missing_kubernetes_namespaces_for_cluster(cluster).each do |project| + create_or_update_namespace(cluster, project) + end + end + + def self.create_or_update_namespaces_for_project(project) + clusters_with_missing_kubernetes_namespaces_for_project(project).each do |cluster| + create_or_update_namespace(cluster, project) + end + end + + def self.projects_with_missing_kubernetes_namespaces_for_cluster(cluster) + cluster.all_projects.missing_kubernetes_namespace(cluster.kubernetes_namespaces) + end + + private_class_method :projects_with_missing_kubernetes_namespaces_for_cluster + + def self.clusters_with_missing_kubernetes_namespaces_for_project(project) + project.all_clusters.missing_kubernetes_namespace(project.kubernetes_namespaces) + end + + private_class_method :clusters_with_missing_kubernetes_namespaces_for_project + + def self.create_or_update_namespace(cluster, project) + kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace_for_project(project) + + ::Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( + cluster: cluster, + kubernetes_namespace: kubernetes_namespace + ).execute + end + + private_class_method :create_or_update_namespace + end +end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 9e77a3237e34219c027d5a486229796f397cc4dc..d03137b63b28b4276353de761b19087425699e70 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -96,6 +96,8 @@ module Projects current_user.invalidate_personal_projects_count create_readme if @initialize_with_readme + + configure_group_clusters_for_project end # Refresh the current user's authorizations inline (so they can access the @@ -121,6 +123,10 @@ module Projects Files::CreateService.new(@project, current_user, commit_attrs).execute end + def configure_group_clusters_for_project + ClusterProjectConfigureWorker.perform_async(@project.id) + end + def skip_wiki? !@project.feature_available?(:wiki, current_user) || @skip_wiki end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 9d40ab166ff5844755097694f34fe73187153549..9db3fd9cf174135813aa4e4d682b9cf77f8e1d03 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -54,6 +54,7 @@ module Projects end attempt_transfer_transaction + configure_group_clusters_for_project end # rubocop: enable CodeReuse/ActiveRecord @@ -162,5 +163,9 @@ module Projects @new_namespace.full_path ) end + + def configure_group_clusters_for_project + ClusterProjectConfigureWorker.perform_async(project.id) + end end end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index c0b410472eba5505d4067162dc7f9cd88e1f8baf..e51da79c6b50de29d5db3428533b27da62565d9f 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -29,6 +29,7 @@ - gcp_cluster:wait_for_cluster_creation - gcp_cluster:cluster_wait_for_ingress_ip_address - gcp_cluster:cluster_platform_configure +- gcp_cluster:cluster_project_configure - github_import_advance_stage - github_importer:github_import_import_diff_note diff --git a/app/workers/cluster_platform_configure_worker.rb b/app/workers/cluster_platform_configure_worker.rb index 8f3689f01666e6edd7d4f0545d1c4cd508019ffe..aa7570caa7902f01ed7908fb204fee0d100dad99 100644 --- a/app/workers/cluster_platform_configure_worker.rb +++ b/app/workers/cluster_platform_configure_worker.rb @@ -6,17 +6,7 @@ class ClusterPlatformConfigureWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - next unless cluster.cluster_project - - kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project) - - Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( - cluster: cluster, - kubernetes_namespace: kubernetes_namespace - ).execute + Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster) end - - rescue ::Kubeclient::HttpError => err - Rails.logger.error "Failed to create/update Kubernetes namespace for cluster_id: #{cluster_id} with error: #{err.message}" end end diff --git a/app/workers/cluster_project_configure_worker.rb b/app/workers/cluster_project_configure_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..497e57c0d0b646b9c92b8ec45ae0c48fdd8029de --- /dev/null +++ b/app/workers/cluster_project_configure_worker.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class ClusterProjectConfigureWorker + include ApplicationWorker + include ClusterQueue + + def perform(project_id) + project = Project.find(project_id) + + ::Clusters::RefreshService.create_or_update_namespaces_for_project(project) + end +end diff --git a/changelogs/unreleased/34758-deployment-cluster.yml b/changelogs/unreleased/34758-deployment-cluster.yml new file mode 100644 index 0000000000000000000000000000000000000000..063740983439d942cb82d3d247bf2d3ec85a55a1 --- /dev/null +++ b/changelogs/unreleased/34758-deployment-cluster.yml @@ -0,0 +1,5 @@ +--- +title: Use group clusters when deploying (DeploymentPlatform) +merge_request: 22308 +author: +type: changed diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb index c940ea7305e1f5ec0098e7d77625dd37b7a950c0..97cbdc6cb39f968e1731b6b5796caf68a56cec04 100644 --- a/lib/gitlab/group_hierarchy.rb +++ b/lib/gitlab/group_hierarchy.rb @@ -34,8 +34,8 @@ module Gitlab # reached. So all ancestors *lower* than the specified ancestor will be # included. # rubocop: disable CodeReuse/ActiveRecord - def ancestors(upto: nil) - base_and_ancestors(upto: upto).where.not(id: ancestors_base.select(:id)) + def ancestors(upto: nil, hierarchy_order: nil) + base_and_ancestors(upto: upto, hierarchy_order: hierarchy_order).where.not(id: ancestors_base.select(:id)) end # rubocop: enable CodeReuse/ActiveRecord @@ -45,11 +45,22 @@ module Gitlab # Passing an `upto` will stop the recursion once the specified parent_id is # reached. So all ancestors *lower* than the specified acestor will be # included. - def base_and_ancestors(upto: nil) + # + # Passing a `hierarchy_order` with either `:asc` or `:desc` will cause the + # recursive query order from most nested group to root or from the root + # ancestor to most nested group respectively. This uses a `depth` column + # where `1` is defined as the depth for the base and increment as we go up + # each parent. + # rubocop: disable CodeReuse/ActiveRecord + def base_and_ancestors(upto: nil, hierarchy_order: nil) return ancestors_base unless Group.supports_nested_groups? - read_only(base_and_ancestors_cte(upto).apply_to(model.all)) + recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(model.all) + recursive_query = recursive_query.order(depth: hierarchy_order) if hierarchy_order + + read_only(recursive_query) end + # rubocop: enable CodeReuse/ActiveRecord # Returns a relation that includes the descendants_base set of groups # and all their descendants (recursively). @@ -107,16 +118,22 @@ module Gitlab private # rubocop: disable CodeReuse/ActiveRecord - def base_and_ancestors_cte(stop_id = nil) + def base_and_ancestors_cte(stop_id = nil, hierarchy_order = nil) cte = SQL::RecursiveCTE.new(:base_and_ancestors) + depth_column = :depth + + base_query = ancestors_base.except(:order) + base_query = base_query.select("1 as #{depth_column}", groups_table[Arel.star]) if hierarchy_order - cte << ancestors_base.except(:order) + cte << base_query # Recursively get all the ancestors of the base set. parent_query = model .from([groups_table, cte.table]) .where(groups_table[:id].eq(cte.table[:parent_id])) .except(:order) + + parent_query = parent_query.select(cte.table[depth_column] + 1, groups_table[Arel.star]) if hierarchy_order parent_query = parent_query.where(cte.table[:parent_id].not_eq(stop_id)) if stop_id cte << parent_query diff --git a/spec/features/ide_spec.rb b/spec/features/ide_spec.rb index 65989c36c1e43107588f8e96e171f3f351ff2c5f..6eb59ef72c255f52d16dc599212b2ac0ec0481b1 100644 --- a/spec/features/ide_spec.rb +++ b/spec/features/ide_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'IDE', :js do - describe 'sub-groups' do + describe 'sub-groups', :nested_groups do let(:user) { create(:user) } let(:group) { create(:group) } let(:subgroup) { create(:group, parent: group) } diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 0add129dde23e2c2267b75ca07ff54509b55ce4b..b56bb272b46cb01e8f370453d85d9ec28562e3ec 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -277,7 +277,7 @@ describe 'Project' do end end - context 'for subgroups', :js do + context 'for subgroups', :js, :nested_groups do let(:group) { create(:group) } let(:subgroup) { create(:group, parent: group) } let(:project) { create(:project, :repository, group: subgroup) } diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb index 30686634af4cb4a1d541f78199d4e24b728203ea..f3de7adcec77d9907c2572006b3b4bcaa6f3c835 100644 --- a/spec/lib/gitlab/group_hierarchy_spec.rb +++ b/spec/lib/gitlab/group_hierarchy_spec.rb @@ -34,6 +34,28 @@ describe Gitlab::GroupHierarchy, :postgresql do expect { relation.update_all(share_with_group_lock: false) } .to raise_error(ActiveRecord::ReadOnlyRecord) end + + describe 'hierarchy_order option' do + let(:relation) do + described_class.new(Group.where(id: child2.id)).base_and_ancestors(hierarchy_order: hierarchy_order) + end + + context ':asc' do + let(:hierarchy_order) { :asc } + + it 'orders by child to parent' do + expect(relation).to eq([child2, child1, parent]) + end + end + + context ':desc' do + let(:hierarchy_order) { :desc } + + it 'orders by parent to child' do + expect(relation).to eq([parent, child1, child2]) + end + end + end end describe '#base_and_descendants' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index d9a4f1960ad1515f0999a4f4446742a17ae1cab6..7df129da95adaa3982236a8e9f9be9b3322789e3 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -307,6 +307,7 @@ project: - import_export_upload - repository_languages - pool_repository +- kubernetes_namespaces award_emoji: - awardable - user diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 7dcf97276b20e45e9fbc7d099e3f4426e8854bbc..840f74c98907f3b89bca238227af17ad2964a582 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -92,6 +92,26 @@ describe Clusters::Cluster do it { is_expected.to contain_exactly(cluster) } end + describe '.missing_kubernetes_namespace' do + let!(:cluster) { create(:cluster, :provided_by_gcp, :project) } + let(:project) { cluster.project } + let(:kubernetes_namespaces) { project.kubernetes_namespaces } + + subject do + described_class.joins(:projects).where(projects: { id: project.id }).missing_kubernetes_namespace(kubernetes_namespaces) + end + + it { is_expected.to contain_exactly(cluster) } + + context 'kubernetes namespace exists' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + it { is_expected.to be_empty } + end + end + describe 'validation' do subject { cluster.valid? } @@ -233,6 +253,81 @@ describe Clusters::Cluster do end end + describe '.ancestor_clusters_for_clusterable' do + let(:group_cluster) { create(:cluster, :provided_by_gcp, :group) } + let(:group) { group_cluster.group } + let(:hierarchy_order) { :desc } + let(:clusterable) { project } + + subject do + described_class.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: hierarchy_order) + end + + context 'when project does not belong to this group' do + let(:project) { create(:project, group: create(:group)) } + + it 'returns nothing' do + is_expected.to be_empty + end + end + + context 'when group has a configured kubernetes cluster' do + let(:project) { create(:project, group: group) } + + it 'returns the group cluster' do + is_expected.to eq([group_cluster]) + end + end + + context 'when sub-group has configured kubernetes cluster', :nested_groups do + let(:sub_group_cluster) { create(:cluster, :provided_by_gcp, :group) } + let(:sub_group) { sub_group_cluster.group } + let(:project) { create(:project, group: sub_group) } + + before do + sub_group.update!(parent: group) + end + + it 'returns clusters in order, descending the hierachy' do + is_expected.to eq([group_cluster, sub_group_cluster]) + end + + it 'avoids N+1 queries' do + another_project = create(:project) + control_count = ActiveRecord::QueryRecorder.new do + described_class.ancestor_clusters_for_clusterable(another_project, hierarchy_order: hierarchy_order) + end.count + + cluster2 = create(:cluster, :provided_by_gcp, :group) + child2 = cluster2.group + child2.update!(parent: sub_group) + project = create(:project, group: child2) + + expect do + described_class.ancestor_clusters_for_clusterable(project, hierarchy_order: hierarchy_order) + end.not_to exceed_query_limit(control_count) + end + + context 'for a group' do + let(:clusterable) { sub_group } + + it 'returns clusters in order for a group' do + is_expected.to eq([group_cluster]) + end + end + end + + context 'scope chaining' do + let(:project) { create(:project, group: group) } + + subject { described_class.none.ancestor_clusters_for_clusterable(project) } + + it 'returns nothing' do + is_expected.to be_empty + end + end + end + describe '#provider' do subject { cluster.provider } @@ -265,6 +360,31 @@ describe Clusters::Cluster do end end + describe '#all_projects' do + let(:project) { create(:project) } + let(:cluster) { create(:cluster, projects: [project]) } + + subject { cluster.all_projects } + + context 'project cluster' do + it 'returns project' do + is_expected.to eq([project]) + end + end + + context 'group cluster' do + let(:cluster) { create(:cluster, :group) } + let(:group) { cluster.group } + let(:project) { create(:project, group: group) } + let(:subgroup) { create(:group, parent: group) } + let(:subproject) { create(:project, group: subgroup) } + + it 'returns all projects for group' do + is_expected.to contain_exactly(project, subproject) + end + end + end + describe '#first_project' do subject { cluster.first_project } diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb index 7bb89fe41dc1be5bd25b9fddcb804fad2b1434fb..19ab4382b53288037238fcc08301c91cf31d92da 100644 --- a/spec/models/concerns/deployment_platform_spec.rb +++ b/spec/models/concerns/deployment_platform_spec.rb @@ -43,13 +43,86 @@ describe DeploymentPlatform do it { is_expected.to be_nil } end - context 'when user configured kubernetes from CI/CD > Clusters' do + context 'when project has configured kubernetes from CI/CD > Clusters' do let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } let(:platform_kubernetes) { cluster.platform_kubernetes } it 'returns the Kubernetes platform' do expect(subject).to eq(platform_kubernetes) end + + context 'with a group level kubernetes cluster' do + let(:group_cluster) { create(:cluster, :provided_by_gcp, :group) } + + before do + project.update!(group: group_cluster.group) + end + + it 'returns the Kubernetes platform from the project cluster' do + expect(subject).to eq(platform_kubernetes) + end + end + end + + context 'when group has configured kubernetes cluster' do + let!(:group_cluster) { create(:cluster, :provided_by_gcp, :group) } + let(:group) { group_cluster.group } + + before do + project.update!(group: group) + end + + it 'returns the Kubernetes platform' do + is_expected.to eq(group_cluster.platform_kubernetes) + end + + context 'when child group has configured kubernetes cluster', :nested_groups do + let!(:child_group1_cluster) { create(:cluster, :provided_by_gcp, :group) } + let(:child_group1) { child_group1_cluster.group } + + before do + project.update!(group: child_group1) + child_group1.update!(parent: group) + end + + it 'returns the Kubernetes platform for the child group' do + is_expected.to eq(child_group1_cluster.platform_kubernetes) + end + + context 'deeply nested group' do + let!(:child_group2_cluster) { create(:cluster, :provided_by_gcp, :group) } + let(:child_group2) { child_group2_cluster.group } + + before do + child_group2.update!(parent: child_group1) + project.update!(group: child_group2) + end + + it 'returns most nested group cluster Kubernetes platform' do + is_expected.to eq(child_group2_cluster.platform_kubernetes) + end + + context 'cluster in the middle of hierarchy is disabled' do + before do + child_group2_cluster.update!(enabled: false) + end + + it 'returns closest enabled Kubenetes platform' do + is_expected.to eq(child_group1_cluster.platform_kubernetes) + end + end + end + end + + context 'feature flag disabled' do + before do + stub_feature_flags(group_clusters: false) + end + + it 'returns nil' do + is_expected.to be_nil + end + end end context 'when user configured kubernetes integration from project services' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index ada00f039286334ce572268fddd8781657616249..0c3a49cd0f2d6cf31405d3afca55ab40e62ec591 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -745,4 +745,33 @@ describe Group do let(:uploader_class) { AttachmentUploader } end end + + describe '#group_clusters_enabled?' do + before do + # Override global stub in spec/spec_helper.rb + expect(Feature).to receive(:enabled?).and_call_original + end + + subject { group.group_clusters_enabled? } + + it { is_expected.to be_truthy } + + context 'explicitly disabled for root ancestor' do + before do + feature = Feature.get(:group_clusters) + feature.disable(group.root_ancestor) + end + + it { is_expected.to be_falsey } + end + + context 'explicitly disabled for root ancestor' do + before do + feature = Feature.get(:group_clusters) + feature.enable(group.root_ancestor) + end + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 2db42fe802a6673881b1c2aa587ebe114415dd28..6ee19c0ddf42ce0eb84a5b2809d17f6b461e20b5 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -560,6 +560,7 @@ describe Namespace do let!(:project2) { create(:project_empty_repo, namespace: child) } it { expect(group.all_projects.to_a).to match_array([project2, project1]) } + it { expect(child.all_projects.to_a).to match_array([project2]) } end describe '#all_pipelines' do @@ -720,6 +721,7 @@ describe Namespace do deep_nested_group = create(:group, parent: nested_group) very_deep_nested_group = create(:group, parent: deep_nested_group) + expect(root_group.root_ancestor).to eq(root_group) expect(nested_group.root_ancestor).to eq(root_group) expect(deep_nested_group.root_ancestor).to eq(root_group) expect(very_deep_nested_group.root_ancestor).to eq(root_group) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index c0bc9eb2ef07d70dac3b967447ffbda7f25de3d7..50920d9d1fc8cb46158ada971173b596405ae5b7 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -87,6 +87,7 @@ describe Project do it { is_expected.to have_many(:pipeline_schedules) } it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:clusters) } + it { is_expected.to have_many(:kubernetes_namespaces) } it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') } it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') } it { is_expected.to have_many(:lfs_file_locks) } @@ -177,6 +178,24 @@ describe Project do it { is_expected.to include_module(Sortable) } end + describe '.missing_kubernetes_namespace' do + let!(:project) { create(:project) } + let!(:cluster) { create(:cluster, :provided_by_user, :group) } + let(:kubernetes_namespaces) { project.kubernetes_namespaces } + + subject { described_class.missing_kubernetes_namespace(kubernetes_namespaces) } + + it { is_expected.to contain_exactly(project) } + + context 'kubernetes namespace exists' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + it { is_expected.to be_empty } + end + end + describe 'validation' do let!(:project) { create(:project) } @@ -416,6 +435,8 @@ describe Project do it { is_expected.to delegate_method(:members).to(:team).with_prefix(true) } it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) } + it { is_expected.to delegate_method(:group_clusters_enabled?).to(:group).with_arguments(allow_nil: true) } + it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) } end describe '#to_reference_with_postfix' do @@ -2121,6 +2142,39 @@ describe Project do it 'includes ancestors upto but excluding the given ancestor' do expect(project.ancestors_upto(parent)).to contain_exactly(child2, child) end + + describe 'with hierarchy_order' do + it 'returns ancestors ordered by descending hierarchy' do + expect(project.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child, child2]) + end + + it 'can be used with upto option' do + expect(project.ancestors_upto(parent, hierarchy_order: :desc)).to eq([child, child2]) + end + end + end + + describe '#root_ancestor' do + let(:project) { create(:project) } + + subject { project.root_ancestor } + + it { is_expected.to eq(project.namespace) } + + context 'in a group' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + + it { is_expected.to eq(group) } + end + + context 'in a nested group', :nested_groups do + let(:root) { create(:group) } + let(:child) { create(:group, parent: root) } + let(:project) { create(:project, group: child) } + + it { is_expected.to eq(root) } + end end describe '#lfs_enabled?' do @@ -4017,6 +4071,27 @@ describe Project do end end + describe '#all_clusters' do + let(:project) { create(:project) } + let(:cluster) { create(:cluster, cluster_type: :project_type, projects: [project]) } + + subject { project.all_clusters } + + it 'returns project level cluster' do + expect(subject).to eq([cluster]) + end + + context 'project belongs to a group' do + let(:group_cluster) { create(:cluster, :group) } + let(:group) { group_cluster.group } + let(:project) { create(:project, group: group) } + + it 'returns clusters for groups of this project' do + expect(subject).to contain_exactly(cluster, group_cluster) + end + end + end + def rugged_config rugged_repo(project.repository).config end diff --git a/spec/services/clusters/refresh_service_spec.rb b/spec/services/clusters/refresh_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..58ab3c3cf73f6a45652098907830987409e75358 --- /dev/null +++ b/spec/services/clusters/refresh_service_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::RefreshService do + shared_examples 'creates a kubernetes namespace' do + let(:token) { 'aaaaaa' } + let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) } + let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) } + + it 'creates a kubernetes namespace' do + expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) + expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) + + expect { subject }.to change(project.kubernetes_namespaces, :count) + + kubernetes_namespace = cluster.kubernetes_namespaces.first + expect(kubernetes_namespace).to be_present + expect(kubernetes_namespace.project).to eq(project) + end + end + + shared_examples 'does not create a kubernetes namespace' do + it 'does not create a new kubernetes namespace' do + expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).not_to receive(:namespace_creator) + expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).not_to receive(:new) + + expect { subject }.not_to change(Clusters::KubernetesNamespace, :count) + end + end + + describe '.create_or_update_namespaces_for_cluster' do + let(:cluster) { create(:cluster, :provided_by_user, :project) } + let(:project) { cluster.project } + + subject { described_class.create_or_update_namespaces_for_cluster(cluster) } + + context 'cluster is project level' do + include_examples 'creates a kubernetes namespace' + + context 'when project already has kubernetes namespace' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + include_examples 'does not create a kubernetes namespace' + end + end + + context 'cluster is group level' do + let(:cluster) { create(:cluster, :provided_by_user, :group) } + let(:group) { cluster.group } + let(:project) { create(:project, group: group) } + + include_examples 'creates a kubernetes namespace' + + context 'when project already has kubernetes namespace' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + include_examples 'does not create a kubernetes namespace' + end + end + end + + describe '.create_or_update_namespaces_for_project' do + let(:project) { create(:project) } + + subject { described_class.create_or_update_namespaces_for_project(project) } + + it 'creates no kubernetes namespaces' do + expect { subject }.not_to change(project.kubernetes_namespaces, :count) + end + + context 'project has a project cluster' do + let!(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :project_type, projects: [project]) } + + include_examples 'creates a kubernetes namespace' + + context 'when project already has kubernetes namespace' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + include_examples 'does not create a kubernetes namespace' + end + end + + context 'project belongs to a group cluster' do + let!(:cluster) { create(:cluster, :provided_by_gcp, :group) } + + let(:group) { cluster.group } + let(:project) { create(:project, group: group) } + + include_examples 'creates a kubernetes namespace' + + context 'when project already has kubernetes namespace' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + include_examples 'does not create a kubernetes namespace' + end + end + end +end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 08de27ca44a3f1ff20dbb73d6e7401da524af227..f71e2b4bc24dd474ae209c368680b244c145265e 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -261,6 +261,32 @@ describe Projects::CreateService, '#execute' do end end + context 'when group has kubernetes cluster' do + let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) } + let(:group) { group_cluster.group } + + let(:token) { 'aaaa' } + let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) } + let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) } + + before do + group.add_owner(user) + + expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) + expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) + end + + it 'creates kubernetes namespace for the project' do + project = create_project(user, opts.merge!(namespace_id: group.id)) + + expect(project).to be_valid + + kubernetes_namespace = group_cluster.kubernetes_namespaces.first + expect(kubernetes_namespace).to be_present + expect(kubernetes_namespace.project).to eq(project) + end + end + context 'when there is an active service template' do before do create(:service, project: nil, template: true, active: true) diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 2e07d4f8013f8a94af0a902d1c80e3266d1233fc..132ad9a2646cdac77778461ffa5dc1f240bd2ec0 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -62,6 +62,32 @@ describe Projects::TransferService do expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" end + + context 'new group has a kubernetes cluster' do + let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) } + let(:group) { group_cluster.group } + + let(:token) { 'aaaa' } + let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) } + let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) } + + subject { transfer_project(project, user, group) } + + before do + expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) + expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) + end + + it 'creates kubernetes namespace for the project' do + subject + + expect(project.kubernetes_namespaces.count).to eq(1) + + kubernetes_namespace = group_cluster.kubernetes_namespaces.first + expect(kubernetes_namespace).to be_present + expect(kubernetes_namespace.project).to eq(project) + end + end end context 'when transfer fails' do diff --git a/spec/workers/cluster_platform_configure_worker_spec.rb b/spec/workers/cluster_platform_configure_worker_spec.rb index b51f6e07c6abf033759e0cb28ef235573a74e760..0eead0ab13dd8c8a783d3990d47cf5b6ace9b0d5 100644 --- a/spec/workers/cluster_platform_configure_worker_spec.rb +++ b/spec/workers/cluster_platform_configure_worker_spec.rb @@ -2,7 +2,43 @@ require 'spec_helper' -describe ClusterPlatformConfigureWorker, '#execute' do +describe ClusterPlatformConfigureWorker, '#perform' do + let(:worker) { described_class.new } + + context 'when group cluster' do + let(:cluster) { create(:cluster, :group, :provided_by_gcp) } + let(:group) { cluster.group } + + context 'when group has no projects' do + it 'does not create a namespace' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:execute) + + worker.perform(cluster.id) + end + end + + context 'when group has a project' do + let!(:project) { create(:project, group: group) } + + it 'creates a namespace for the project' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once + + worker.perform(cluster.id) + end + end + + context 'when group has project in a sub-group' do + let!(:subgroup) { create(:group, parent: group) } + let!(:project) { create(:project, group: subgroup) } + + it 'creates a namespace for the project' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once + + worker.perform(cluster.id) + end + end + end + context 'when provider type is gcp' do let(:cluster) { create(:cluster, :project, :provided_by_gcp) } @@ -30,18 +66,4 @@ describe ClusterPlatformConfigureWorker, '#execute' do described_class.new.perform(123) end end - - context 'when kubeclient raises error' do - let(:cluster) { create(:cluster, :project) } - - it 'rescues and logs the error' do - allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).and_raise(::Kubeclient::HttpError.new(500, 'something baaaad happened', '')) - - expect(Rails.logger) - .to receive(:error) - .with("Failed to create/update Kubernetes namespace for cluster_id: #{cluster.id} with error: something baaaad happened") - - described_class.new.perform(cluster.id) - end - end end