diff options
author | Thong Kuah <tkuah@gitlab.com> | 2018-10-29 05:28:36 +0300 |
---|---|---|
committer | Thong Kuah <tkuah@gitlab.com> | 2018-12-05 00:16:44 +0300 |
commit | 5bb2814ae6eb45463bb1fa4c5ed5fdd376afa2ca (patch) | |
tree | b8442b8ecaabd9db1e32dfe8fdcebd8ea93c8025 | |
parent | d3866fb48cdf640cd16d48b9825dba2c261a78f3 (diff) |
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
-rw-r--r-- | app/models/clusters/cluster.rb | 10 | ||||
-rw-r--r-- | app/models/concerns/deployment_platform.rb | 13 | ||||
-rw-r--r-- | changelogs/unreleased/34758-deployment-cluster.yml | 5 | ||||
-rw-r--r-- | lib/gitlab/group_hierarchy.rb | 23 | ||||
-rw-r--r-- | spec/lib/gitlab/group_hierarchy_spec.rb | 10 | ||||
-rw-r--r-- | spec/models/clusters/cluster_spec.rb | 47 | ||||
-rw-r--r-- | spec/models/concerns/deployment_platform_spec.rb | 77 |
7 files changed, 180 insertions, 5 deletions
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 13906c903b9..a3d26d62a22 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -86,6 +86,16 @@ module Clusters 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 if provider provider.status_name diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index e57a3383544..d785e0d505f 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -13,6 +13,7 @@ module DeploymentPlatform def find_deployment_platform(environment) find_cluster_platform_kubernetes(environment: environment) || + find_group_cluster_platform_kubernetes_with_feature_guard(environment: environment) || find_kubernetes_service_integration || build_cluster_and_deployment_platform end @@ -23,6 +24,18 @@ module DeploymentPlatform .last&.platform_kubernetes end + def find_group_cluster_platform_kubernetes_with_feature_guard(environment: nil) + return unless 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 services.deployment.reorder(nil).find_by(active: true) end diff --git a/changelogs/unreleased/34758-deployment-cluster.yml b/changelogs/unreleased/34758-deployment-cluster.yml new file mode 100644 index 00000000000..06374098343 --- /dev/null +++ b/changelogs/unreleased/34758-deployment-cluster.yml @@ -0,0 +1,5 @@ +--- +title: Use group clusters when deploying (DeploymentPlatform) +merge_request: 22308 +author: +type: changed diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb index c940ea7305e..2502887065f 100644 --- a/lib/gitlab/group_hierarchy.rb +++ b/lib/gitlab/group_hierarchy.rb @@ -45,11 +45,21 @@ 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 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? - 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 + # rubocop: enable CodeReuse/ActiveRecord # Returns a relation that includes the descendants_base set of groups # and all their descendants (recursively). @@ -107,16 +117,21 @@ module Gitlab private # 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 << 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. 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] + 1, groups_table[Arel.star]) if depth parent_query = parent_query.where(cte.table[:parent_id].not_eq(stop_id)) if stop_id cte << parent_query diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb index 30686634af4..edc03ff2e59 100644 --- a/spec/lib/gitlab/group_hierarchy_spec.rb +++ b/spec/lib/gitlab/group_hierarchy_spec.rb @@ -34,6 +34,16 @@ describe Gitlab::GroupHierarchy, :postgresql do expect { relation.update_all(share_with_group_lock: false) } .to raise_error(ActiveRecord::ReadOnlyRecord) 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 describe '#base_and_descendants' do diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 7dcf97276b2..c24c796f0ad 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -233,6 +233,53 @@ describe Clusters::Cluster do 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 subject { cluster.provider } diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb index 7bb89fe41dc..bdd352c52b8 100644 --- a/spec/models/concerns/deployment_platform_spec.rb +++ b/spec/models/concerns/deployment_platform_spec.rb @@ -43,13 +43,88 @@ 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 + 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 context 'when user configured kubernetes integration from project services' do |