Merge branch '34758-deployment-cluster' into 'master'
Use group clusters when deploying (DeploymentPlatform) See merge request gitlab-org/gitlab-ce!22308
This commit is contained in:
commit
13b1508c3e
28 changed files with 693 additions and 48 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
40
app/services/clusters/refresh_service.rb
Normal file
40
app/services/clusters/refresh_service.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
12
app/workers/cluster_project_configure_worker.rb
Normal file
12
app/workers/cluster_project_configure_worker.rb
Normal file
|
@ -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
|
5
changelogs/unreleased/34758-deployment-cluster.yml
Normal file
5
changelogs/unreleased/34758-deployment-cluster.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Use group clusters when deploying (DeploymentPlatform)
|
||||
merge_request: 22308
|
||||
author:
|
||||
type: changed
|
|
@ -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
|
||||
|
||||
cte << ancestors_base.except(:order)
|
||||
base_query = ancestors_base.except(:order)
|
||||
base_query = base_query.select("1 as #{depth_column}", groups_table[Arel.star]) if hierarchy_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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -307,6 +307,7 @@ project:
|
|||
- import_export_upload
|
||||
- repository_languages
|
||||
- pool_repository
|
||||
- kubernetes_namespaces
|
||||
award_emoji:
|
||||
- awardable
|
||||
- user
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
107
spec/services/clusters/refresh_service_spec.rb
Normal file
107
spec/services/clusters/refresh_service_spec.rb
Normal file
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue