Commit 13b1508c authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '34758-deployment-cluster' into 'master'

Use group clusters when deploying (DeploymentPlatform)

See merge request gitlab-org/gitlab-ce!22308
parents 2ea69008 e3188eb1
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
class Groups::ClustersController < Clusters::ClustersController class Groups::ClustersController < Clusters::ClustersController
include ControllerWithCrossProjectAccessCheck include ControllerWithCrossProjectAccessCheck
prepend_before_action :check_group_clusters_feature_flag!
prepend_before_action :group prepend_before_action :group
prepend_before_action :check_group_clusters_feature_flag!
requires_cross_project_access requires_cross_project_access
layout 'group' layout 'group'
...@@ -20,6 +20,10 @@ class Groups::ClustersController < Clusters::ClustersController ...@@ -20,6 +20,10 @@ class Groups::ClustersController < Clusters::ClustersController
end end
def check_group_clusters_feature_flag! 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
end end
...@@ -140,7 +140,7 @@ module GroupsHelper ...@@ -140,7 +140,7 @@ module GroupsHelper
can?(current_user, "read_group_#{resource}".to_sym, @group) can?(current_user, "read_group_#{resource}".to_sym, @group)
end 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 links << :kubernetes
end end
......
...@@ -4,6 +4,7 @@ module Clusters ...@@ -4,6 +4,7 @@ module Clusters
class Cluster < ActiveRecord::Base class Cluster < ActiveRecord::Base
include Presentable include Presentable
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include FromUnion
self.table_name = 'clusters' self.table_name = 'clusters'
...@@ -86,6 +87,19 @@ module Clusters ...@@ -86,6 +87,19 @@ module Clusters
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) } 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 def status_name
if provider if provider
provider.status_name provider.status_name
...@@ -122,6 +136,16 @@ module Clusters ...@@ -122,6 +136,16 @@ module Clusters
!user? !user?
end end
def all_projects
if project_type?
projects
elsif group_type?
first_group.all_projects
else
Project.none
end
end
def first_project def first_project
strong_memoize(:first_project) do strong_memoize(:first_project) do
projects.first projects.first
...@@ -140,11 +164,17 @@ module Clusters ...@@ -140,11 +164,17 @@ module Clusters
platform_kubernetes.kubeclient if kubernetes? platform_kubernetes.kubeclient if kubernetes?
end end
def find_or_initialize_kubernetes_namespace(cluster_project) def find_or_initialize_kubernetes_namespace_for_project(project)
if project_type?
kubernetes_namespaces.find_or_initialize_by( kubernetes_namespaces.find_or_initialize_by(
project: cluster_project.project, project: project,
cluster_project: cluster_project cluster_project: cluster_project
) )
else
kubernetes_namespaces.find_or_initialize_by(
project: project
)
end
end end
def allow_user_defined_namespace? def allow_user_defined_namespace?
......
...@@ -13,6 +13,7 @@ module DeploymentPlatform ...@@ -13,6 +13,7 @@ module DeploymentPlatform
def find_deployment_platform(environment) def find_deployment_platform(environment)
find_cluster_platform_kubernetes(environment: environment) || find_cluster_platform_kubernetes(environment: environment) ||
find_group_cluster_platform_kubernetes_with_feature_guard(environment: environment) ||
find_kubernetes_service_integration || find_kubernetes_service_integration ||
build_cluster_and_deployment_platform build_cluster_and_deployment_platform
end end
...@@ -23,6 +24,18 @@ module DeploymentPlatform ...@@ -23,6 +24,18 @@ module DeploymentPlatform
.last&.platform_kubernetes .last&.platform_kubernetes
end 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 def find_kubernetes_service_integration
services.deployment.reorder(nil).find_by(active: true) services.deployment.reorder(nil).find_by(active: true)
end end
......
...@@ -400,6 +400,10 @@ class Group < Namespace ...@@ -400,6 +400,10 @@ class Group < Namespace
ensure_runners_token! ensure_runners_token!
end end
def group_clusters_enabled?
Feature.enabled?(:group_clusters, root_ancestor, default_enabled: true)
end
private private
def update_two_factor_requirement def update_two_factor_requirement
......
...@@ -192,9 +192,9 @@ class Namespace < ActiveRecord::Base ...@@ -192,9 +192,9 @@ class Namespace < ActiveRecord::Base
# returns all ancestors upto but excluding the given namespace # returns all ancestors upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned # 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)) Gitlab::GroupHierarchy.new(self.class.where(id: id))
.ancestors(upto: top) .ancestors(upto: top, hierarchy_order: hierarchy_order)
end end
def self_and_ancestors def self_and_ancestors
...@@ -243,7 +243,7 @@ class Namespace < ActiveRecord::Base ...@@ -243,7 +243,7 @@ class Namespace < ActiveRecord::Base
end end
def root_ancestor def root_ancestor
ancestors.reorder(nil).find_by(parent_id: nil) self_and_ancestors.reorder(nil).find_by(parent_id: nil)
end end
def subgroup? def subgroup?
......
...@@ -238,6 +238,7 @@ class Project < ActiveRecord::Base ...@@ -238,6 +238,7 @@ class Project < ActiveRecord::Base
has_one :cluster_project, class_name: 'Clusters::Project' has_one :cluster_project, class_name: 'Clusters::Project'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster' 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 :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace'
has_many :prometheus_metrics has_many :prometheus_metrics
...@@ -300,6 +301,8 @@ class Project < ActiveRecord::Base ...@@ -300,6 +301,8 @@ class Project < ActiveRecord::Base
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team
delegate :add_master, to: :team # @deprecated delegate :add_master, to: :team # @deprecated
delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings 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 # Validations
validates :creator, presence: true, on: :create validates :creator, presence: true, on: :create
...@@ -392,6 +395,12 @@ class Project < ActiveRecord::Base ...@@ -392,6 +395,12 @@ class Project < ActiveRecord::Base
.where(project_ci_cd_settings: { group_runners_enabled: true }) .where(project_ci_cd_settings: { group_runners_enabled: true })
end 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 } enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout, chronic_duration_attr :build_timeout_human_readable, :build_timeout,
...@@ -556,9 +565,9 @@ class Project < ActiveRecord::Base ...@@ -556,9 +565,9 @@ class Project < ActiveRecord::Base
# returns all ancestor-groups upto but excluding the given namespace # returns all ancestor-groups upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned # 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)) Gitlab::GroupHierarchy.new(Group.where(id: namespace_id))
.base_and_ancestors(upto: top) .base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
end end
def lfs_enabled? def lfs_enabled?
...@@ -1071,6 +1080,12 @@ class Project < ActiveRecord::Base ...@@ -1071,6 +1080,12 @@ class Project < ActiveRecord::Base
path path
end 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) def items_for(entity)
case entity case entity
when 'issue' then when 'issue' then
......
# 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
...@@ -96,6 +96,8 @@ module Projects ...@@ -96,6 +96,8 @@ module Projects
current_user.invalidate_personal_projects_count current_user.invalidate_personal_projects_count
create_readme if @initialize_with_readme create_readme if @initialize_with_readme
configure_group_clusters_for_project
end end
# Refresh the current user's authorizations inline (so they can access the # Refresh the current user's authorizations inline (so they can access the
...@@ -121,6 +123,10 @@ module Projects ...@@ -121,6 +123,10 @@ module Projects
Files::CreateService.new(@project, current_user, commit_attrs).execute Files::CreateService.new(@project, current_user, commit_attrs).execute
end end
def configure_group_clusters_for_project
ClusterProjectConfigureWorker.perform_async(@project.id)
end
def skip_wiki? def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki !@project.feature_available?(:wiki, current_user) || @skip_wiki
end end
......
...@@ -54,6 +54,7 @@ module Projects ...@@ -54,6 +54,7 @@ module Projects
end end
attempt_transfer_transaction attempt_transfer_transaction
configure_group_clusters_for_project
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -162,5 +163,9 @@ module Projects ...@@ -162,5 +163,9 @@ module Projects
@new_namespace.full_path @new_namespace.full_path
) )
end end
def configure_group_clusters_for_project
ClusterProjectConfigureWorker.perform_async(project.id)
end
end end
end end
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
- gcp_cluster:wait_for_cluster_creation - gcp_cluster:wait_for_cluster_creation
- gcp_cluster:cluster_wait_for_ingress_ip_address - gcp_cluster:cluster_wait_for_ingress_ip_address
- gcp_cluster:cluster_platform_configure - gcp_cluster:cluster_platform_configure
- gcp_cluster:cluster_project_configure
- github_import_advance_stage - github_import_advance_stage
- github_importer:github_import_import_diff_note - github_importer:github_import_import_diff_note
......
...@@ -6,17 +6,7 @@ class ClusterPlatformConfigureWorker ...@@ -6,17 +6,7 @@ class ClusterPlatformConfigureWorker
def perform(cluster_id) def perform(cluster_id)
Clusters::Cluster.find_by_id(cluster_id).try do |cluster| Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
next unless cluster.cluster_project Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster)
kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
cluster: cluster,
kubernetes_namespace: kubernetes_namespace
).execute
end 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
end end
# 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
---
title: Use group clusters when deploying (DeploymentPlatform)
merge_request: 22308
author:
type: changed
...@@ -34,8 +34,8 @@ module Gitlab ...@@ -34,8 +34,8 @@ module Gitlab
# reached. So all ancestors *lower* than the specified ancestor will be # reached. So all ancestors *lower* than the specified ancestor will be
# included. # included.
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def ancestors(upto: nil) def ancestors(upto: nil, hierarchy_order: nil)
base_and_ancestors(upto: upto).where.not(id: ancestors_base.select(:id)) base_and_ancestors(upto: upto, hierarchy_order: hierarchy_order).where.not(id: ancestors_base.select(:id))
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -45,11 +45,22 @@ module Gitlab ...@@ -45,11 +45,22 @@ module Gitlab
# Passing an `upto` will stop the recursion once the specified parent_id is # Passing an `upto` will stop the recursion once the specified parent_id is
# reached. So all ancestors *lower* than the specified acestor will be # reached. So all ancestors *lower* than the specified acestor will be
# included. # 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? 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 end
# rubocop: enable CodeReuse/ActiveRecord
# Returns a relation that includes the descendants_base set of groups # Returns a relation that includes the descendants_base set of groups
# and all their descendants (recursively). # and all their descendants (recursively).
...@@ -107,16 +118,22 @@ module Gitlab ...@@ -107,16 +118,22 @@ module Gitlab
private private
# rubocop: disable CodeReuse/ActiveRecord # 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) 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. # Recursively get all the ancestors of the base set.
parent_query = model parent_query = model
.from([groups_table, cte.table]) .from([groups_table, cte.table])
.where(groups_table[:id].eq(cte.table[:parent_id])) .where(groups_table[:id].eq(cte.table[:parent_id]))
.except(:order) .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 parent_query = parent_query.where(cte.table[:parent_id].not_eq(stop_id)) if stop_id
cte << parent_query cte << parent_query
......
require 'spec_helper' require 'spec_helper'
describe 'IDE', :js do describe 'IDE', :js do
describe 'sub-groups' do describe 'sub-groups', :nested_groups do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) } let(:subgroup) { create(:group, parent: group) }
......
...@@ -277,7 +277,7 @@ describe 'Project' do ...@@ -277,7 +277,7 @@ describe 'Project' do
end end
end end
context 'for subgroups', :js do context 'for subgroups', :js, :nested_groups do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) } let(:subgroup) { create(:group, parent: group) }
let(:project) { create(:project, :repository, group: subgroup) } let(:project) { create(:project, :repository, group: subgroup) }
......
...@@ -34,6 +34,28 @@ describe Gitlab::GroupHierarchy, :postgresql do ...@@ -34,6 +34,28 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect { relation.update_all(share_with_group_lock: false) } expect { relation.update_all(share_with_group_lock: false) }
.to raise_error(ActiveRecord::ReadOnlyRecord) .to raise_error(ActiveRecord::ReadOnlyRecord)
end 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 end
describe '#base_and_descendants' do describe '#base_and_descendants' do
......
...@@ -307,6 +307,7 @@ project: ...@@ -307,6 +307,7 @@ project:
- import_export_upload - import_export_upload
- repository_languages - repository_languages
- pool_repository - pool_repository
- kubernetes_namespaces
award_emoji: award_emoji:
- awardable - awardable
- user - user
......
...@@ -92,6 +92,26 @@ describe Clusters::Cluster do ...@@ -92,6 +92,26 @@ describe Clusters::Cluster do
it { is_expected.to contain_exactly(cluster) } it { is_expected.to contain_exactly(cluster) }
end 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 describe 'validation' do
subject { cluster.valid? } subject { cluster.valid? }
...@@ -233,6 +253,81 @@ describe Clusters::Cluster do ...@@ -233,6 +253,81 @@ describe Clusters::Cluster do
end end
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 describe '#provider' do
subject { cluster.provider } subject { cluster.provider }
...@@ -265,6 +360,31 @@ describe Clusters::Cluster do ...@@ -265,6 +360,31 @@ describe Clusters::Cluster do
end end
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 describe '#first_project' do
subject { cluster.first_project } subject { cluster.first_project }
......
...@@ -43,13 +43,86 @@ describe DeploymentPlatform do ...@@ -43,13 +43,86 @@ describe DeploymentPlatform do
it { is_expected.to be_nil } it { is_expected.to be_nil }
end 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!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let(:platform_kubernetes) { cluster.platform_kubernetes } let(:platform_kubernetes) { cluster.platform_kubernetes }
it 'returns the Kubernetes platform' do it 'returns the Kubernetes platform' do
expect(subject).to eq(platform_kubernetes) expect(subject).to eq(platform_kubernetes)
end 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 end
context 'when user configured kubernetes integration from project services' do context 'when user configured kubernetes integration from project services' do
......
...@@ -745,4 +745,33 @@ describe Group do ...@@ -745,4 +745,33 @@ describe Group do
let(:uploader_class) { AttachmentUploader } let(:uploader_class) { AttachmentUploader }
end end
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 end
...@@ -560,6 +560,7 @@ describe Namespace do ...@@ -560,6 +560,7 @@ describe Namespace do
let!(:project2) { create(:project_empty_repo, namespace: child) } let!(:project2) { create(:project_empty_repo, namespace: child) }
it { expect(group.all_projects.to_a).to match_array([project2, project1]) } it { expect(group.all_projects.to_a).to match_array([project2, project1]) }
it { expect(child.all_projects.to_a).to match_array([project2]) }
end end
describe '#all_pipelines' do describe '#all_pipelines' do
...@@ -720,6 +721,7 @@ describe Namespace do ...@@ -720,6 +721,7 @@ describe Namespace do
deep_nested_group = create(:group, parent: nested_group) deep_nested_group = create(:group, parent: nested_group)
very_deep_nested_group = create(:group, parent: deep_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(nested_group.root_ancestor).to eq(root_group)
expect(deep_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) expect(very_deep_nested_group.root_ancestor).to eq(root_group)
......
...@@ -87,6 +87,7 @@ describe Project do ...@@ -87,6 +87,7 @@ describe Project do
it { is_expected.to have_many(:pipeline_schedules) } it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:clusters) } 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(:custom_attributes).class_name('ProjectCustomAttribute') }
it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') } it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
it { is_expected.to have_many(:lfs_file_locks) } it { is_expected.to have_many(:lfs_file_locks) }
...@@ -177,6 +178,24 @@ describe Project do ...@@ -177,6 +178,24 @@ describe Project do
it { is_expected.to include_module(Sortable) } it { is_expected.to include_module(Sortable) }
end 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 describe 'validation' do
let!(:project) { create(:project) } let!(:project) { create(:project) }
...@@ -416,6 +435,8 @@ describe Project do ...@@ -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(: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(: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 end
describe '#to_reference_with_postfix' do describe '#to_reference_with_postfix' do
...@@ -2121,6 +2142,39 @@ describe Project do ...@@ -2121,6 +2142,39 @@ describe Project do
it 'includes ancestors upto but excluding the given ancestor' do it 'includes ancestors upto but excluding the given ancestor' do
expect(project.ancestors_upto(parent)).to contain_exactly(child2, child) expect(project.ancestors_upto(parent)).to contain_exactly(child2, child)
end 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 end
describe '#lfs_enabled?' do describe '#lfs_enabled?' do
...@@ -4017,6 +4071,27 @@ describe Project do ...@@ -4017,6 +4071,27 @@ describe Project do
end end
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 def rugged_config
rugged_repo(project.repository).config rugged_repo(project.repository).config
end end
......
# 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
...@@ -261,6 +261,32 @@ describe Projects::CreateService, '#execute' do ...@@ -261,6 +261,32 @@ describe Projects::CreateService, '#execute' do
end end
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 context 'when there is an active service template' do
before do before do
create(:service, project: nil, template: true, active: true) create(:service, project: nil, template: true, active: true)
......
...@@ -62,6 +62,32 @@ describe Projects::TransferService do ...@@ -62,6 +62,32 @@ describe Projects::TransferService do
expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}"
end 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 end
context 'when transfer fails' do context 'when transfer fails' do
......
...@@ -2,7 +2,43 @@ ...@@ -2,7 +2,43 @@
require 'spec_helper' 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 context 'when provider type is gcp' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
...@@ -30,18 +66,4 @@ describe ClusterPlatformConfigureWorker, '#execute' do ...@@ -30,18 +66,4 @@ describe ClusterPlatformConfigureWorker, '#execute' do
described_class.new.perform(123) described_class.new.perform(123)
end end
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 end
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