Commit 5bb2814a authored by Thong Kuah's avatar Thong Kuah

Deploy to clusters for a project's groups

Look for matching clusters starting from the closest ancestor, then go
up the ancestor tree.

Then use Ruby to get clusters for each group in order. Not that
efficient, considering we will doing up to `NUMBER_OF_ANCESTORS_ALLOWED`
number of queries, but it's a finite number

Explicitly order query by depth

This allows us to control ordering explicitly and also to reverse the
order which is useful to allow us to be consistent with
Clusters::Cluster.on_environment (EE) which does reverse ordering.

Puts querying group clusters behind Feature Flag. Just in case we have
issues with performance, we can easily disable this
parent d3866fb4
...@@ -86,6 +86,16 @@ module Clusters ...@@ -86,6 +86,16 @@ module Clusters
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) } scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
# Returns an ordered list of group clusters order from clusters of closest
# group up to furthest ancestor group
def self.ordered_group_clusters_for_project(project_id)
project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
hierarchy_groups = Gitlab::GroupHierarchy.new(project_groups)
.base_and_ancestors(depth: :desc)
hierarchy_groups.flat_map(&:clusters)
end
def status_name def status_name
if provider if provider
provider.status_name provider.status_name
......
...@@ -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 Feature.enabled?(:deploy_group_clusters, default_enabled: true)
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.ordered_group_clusters_for_project(id)
.last&.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
......
---
title: Use group clusters when deploying (DeploymentPlatform)
merge_request: 22308
author:
type: changed
...@@ -45,11 +45,21 @@ module Gitlab ...@@ -45,11 +45,21 @@ 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 an `depth` with either `:asc` or `:desc` will cause the recursive
# query to use a depth column to order by depth (`:asc` returns most nested
# group to root; `desc` returns opposite order). We define 1 as the depth
# for the base and increment as we go up each parent.
# rubocop: disable CodeReuse/ActiveRecord
def base_and_ancestors(upto: nil, depth: 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, depth).apply_to(model.all)
recursive_query = recursive_query.order(depth: depth) if depth
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 +117,21 @@ module Gitlab ...@@ -107,16 +117,21 @@ 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, depth = nil)
cte = SQL::RecursiveCTE.new(:base_and_ancestors) cte = SQL::RecursiveCTE.new(:base_and_ancestors)
cte << ancestors_base.except(:order) base_query = ancestors_base.except(:order)
base_query = base_query.select('1 AS depth', groups_table[Arel.star]) if depth
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] + 1, groups_table[Arel.star]) if depth
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
......
...@@ -34,6 +34,16 @@ describe Gitlab::GroupHierarchy, :postgresql do ...@@ -34,6 +34,16 @@ 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
context 'with depth option' do
let(:relation) do
described_class.new(Group.where(id: child2.id)).base_and_ancestors(depth: :asc)
end
it 'orders by depth' do
expect(relation.map(&:depth)).to eq([1, 2, 3])
end
end
end end
describe '#base_and_descendants' do describe '#base_and_descendants' do
......
...@@ -233,6 +233,53 @@ describe Clusters::Cluster do ...@@ -233,6 +233,53 @@ describe Clusters::Cluster do
end end
end end
describe '.ordered_group_clusters_for_project' do
let(:group_cluster) { create(:cluster, :provided_by_gcp, :group) }
let(:group) { group_cluster.group }
subject { described_class.ordered_group_clusters_for_project(project.id) }
context 'when project does not belong to this group' do
let(:project) { create(:project, group: create(:group)) }
it 'returns nothing' do
expect(subject).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
expect(subject).to eq([group_cluster])
end
end
context 'when sub-group has configured kubernetes cluster', :postgresql 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, ascending the hierachy' do
expect(subject).to eq([group_cluster, sub_group_cluster])
end
end
context 'cluster_scope arg' do
let(:project) { create(:project, group: group) }
subject { described_class.none.ordered_group_clusters_for_project(project.id) }
it 'returns nothing' do
expect(subject).to be_empty
end
end
end
describe '#provider' do describe '#provider' do
subject { cluster.provider } subject { cluster.provider }
......
...@@ -43,13 +43,88 @@ describe DeploymentPlatform do ...@@ -43,13 +43,88 @@ 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
stub_feature_flags(deploy_group_clusters: true)
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(deploy_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
......
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