From 5bb2814ae6eb45463bb1fa4c5ed5fdd376afa2ca Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Mon, 29 Oct 2018 15:28:36 +1300 Subject: 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 --- spec/models/clusters/cluster_spec.rb | 47 +++++++++++++++ spec/models/concerns/deployment_platform_spec.rb | 77 +++++++++++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) (limited to 'spec/models') 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 -- cgit v1.2.3 From e9eccee9ae7be13f73b61f24d4cc6b52af997464 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Fri, 23 Nov 2018 16:19:53 +1300 Subject: Assert all_projects work for child groups --- spec/models/namespace_spec.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'spec/models') diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 2db42fe802a..84930d11a06 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -560,6 +560,7 @@ describe Namespace do let!(:project2) { create(:project_empty_repo, namespace: child) } it { expect(group.all_projects.to_a).to match_array([project2, project1]) } + it { expect(child.all_projects.to_a).to match_array([project2]) } end describe '#all_pipelines' do -- cgit v1.2.3 From 703233e1e9ce8572740b7f22ea62b2a59015e564 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Fri, 23 Nov 2018 16:28:16 +1300 Subject: Add association project -> kubernetes_namespaces kubernetes_namespaces is not needed for project import/export as it tracks internal state of kubernetes integration --- spec/models/project_spec.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'spec/models') diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 3f99fd12e38..edb9ff4e734 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -87,6 +87,7 @@ describe Project do it { is_expected.to have_many(:pipeline_schedules) } it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:clusters) } + it { is_expected.to have_many(:kubernetes_namespaces) } it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') } it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') } it { is_expected.to have_many(:lfs_file_locks) } -- cgit v1.2.3 From 9c5977c821c1958709227a7a5976e1faedc53923 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Sun, 25 Nov 2018 21:26:28 +1300 Subject: Teach Project about #all_clusters This returns a union of the project level clusters and group level clusters associated with this project. --- spec/models/project_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'spec/models') diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index edb9ff4e734..5cce8d87c96 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3984,6 +3984,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 -- cgit v1.2.3 From 8419b7dd2b85fbe9216a31ce84d5ecb234a8b90a Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Mon, 26 Nov 2018 22:01:18 +1300 Subject: Teach Cluster about #all_projects For project level, it's the project directly associated. For group level, it's the projects under that group. --- spec/models/clusters/cluster_spec.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'spec/models') diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index c24c796f0ad..244cd125967 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -312,6 +312,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 } -- cgit v1.2.3 From d54791e0942ae774876db22675cde1b54f35109d Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Thu, 15 Nov 2018 22:17:41 +1300 Subject: Create k8s namespace for project in group clusters AFAIK the only relevant place is Projects::CreateService, this gets called when user creates a new project, forks a new project and does those things via the api. Also create k8s namespace for new group hierarchy when transferring project between groups Uses new Refresh service to create k8s namespaces - Ensure we use Cluster#cluster_project If a project has multiple clusters (EE), using Project#cluster_project is not guaranteed to return the cluster_project for this cluster. So switch to using Cluster#cluster_project - at this stage a cluster can only have 1 cluster_project. Also, remove rescue so that sidekiq can retry --- spec/models/clusters/cluster_spec.rb | 20 ++++++++++++++++++++ spec/models/project_spec.rb | 18 ++++++++++++++++++ 2 files changed, 38 insertions(+) (limited to 'spec/models') diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 244cd125967..229956b56b2 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -92,6 +92,26 @@ describe Clusters::Cluster do it { is_expected.to contain_exactly(cluster) } end + describe '.missing_kubernetes_namespace' do + let!(:cluster) { create(:cluster, :provided_by_gcp, :project) } + let(:project) { cluster.project } + let(:kubernetes_namespaces) { project.kubernetes_namespaces } + + subject do + described_class.joins(:projects).where(projects: { id: project.id }).missing_kubernetes_namespace(kubernetes_namespaces) + end + + it { is_expected.to contain_exactly(cluster) } + + context 'kubernetes namespace exists' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + it { is_expected.to be_empty } + end + end + describe 'validation' do subject { cluster.valid? } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5cce8d87c96..8b1743def72 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -155,6 +155,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) } -- cgit v1.2.3 From f85440e63c2eba453e09a5599f0d3e0491a037f1 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Mon, 3 Dec 2018 10:22:33 +1300 Subject: Various improvements to hierarchy sorting - Rename ordered_group_clusters_for_project -> ancestor_clusters_for_clusterable - Improve name of order option. It makes much more sense to have `hierarchy_order: :asc` and `hierarchy_order: :desc` - Allow ancestor_clusters_for_clusterable for group - Re-use code already present in Project --- spec/models/clusters/cluster_spec.rb | 32 ++++++++++++++++++++++---------- spec/models/project_spec.rb | 10 ++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) (limited to 'spec/models') diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 229956b56b2..adb0ea3a444 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -253,17 +253,21 @@ describe Clusters::Cluster do end end - describe '.ordered_group_clusters_for_project' do + 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 { described_class.ordered_group_clusters_for_project(project.id) } + 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 - expect(subject).to be_empty + is_expected.to be_empty end end @@ -271,11 +275,11 @@ describe Clusters::Cluster do let(:project) { create(:project, group: group) } it 'returns the group cluster' do - expect(subject).to eq([group_cluster]) + is_expected.to eq([group_cluster]) end end - context 'when sub-group has configured kubernetes cluster', :postgresql do + 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) } @@ -284,18 +288,26 @@ describe Clusters::Cluster 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]) + it 'returns clusters in order, descending the hierachy' do + is_expected.to eq([group_cluster, sub_group_cluster]) + 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 'cluster_scope arg' do + context 'scope chaining' do let(:project) { create(:project, group: group) } - subject { described_class.none.ordered_group_clusters_for_project(project.id) } + subject { described_class.none.ancestor_clusters_for_clusterable(project) } it 'returns nothing' do - expect(subject).to be_empty + is_expected.to be_empty end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8b1743def72..68dcb8b3e22 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2117,6 +2117,16 @@ 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 '#lfs_enabled?' do -- cgit v1.2.3 From ebf87fd9c2e1c5bde72f2c08db0fff7e81882cb8 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Thu, 29 Nov 2018 13:50:19 +1300 Subject: Unify into :group_clusters feature flag With this MR, group clusters is now functional, so default to enabled. Have a single setting on the root ancestor group to enabled or disable group clusters feature as a whole --- spec/models/concerns/deployment_platform_spec.rb | 4 +--- spec/models/group_spec.rb | 29 ++++++++++++++++++++++++ spec/models/namespace_spec.rb | 1 + spec/models/project_spec.rb | 25 ++++++++++++++++++++ 4 files changed, 56 insertions(+), 3 deletions(-) (limited to 'spec/models') diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb index bdd352c52b8..19ab4382b53 100644 --- a/spec/models/concerns/deployment_platform_spec.rb +++ b/spec/models/concerns/deployment_platform_spec.rb @@ -69,8 +69,6 @@ describe DeploymentPlatform do let(:group) { group_cluster.group } before do - stub_feature_flags(deploy_group_clusters: true) - project.update!(group: group) end @@ -118,7 +116,7 @@ describe DeploymentPlatform do context 'feature flag disabled' do before do - stub_feature_flags(deploy_group_clusters: false) + stub_feature_flags(group_clusters: false) end it 'returns nil' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index ada00f03928..0c3a49cd0f2 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -745,4 +745,33 @@ describe Group do let(:uploader_class) { AttachmentUploader } end end + + describe '#group_clusters_enabled?' do + before do + # Override global stub in spec/spec_helper.rb + expect(Feature).to receive(:enabled?).and_call_original + end + + subject { group.group_clusters_enabled? } + + it { is_expected.to be_truthy } + + context 'explicitly disabled for root ancestor' do + before do + feature = Feature.get(:group_clusters) + feature.disable(group.root_ancestor) + end + + it { is_expected.to be_falsey } + end + + context 'explicitly disabled for root ancestor' do + before do + feature = Feature.get(:group_clusters) + feature.enable(group.root_ancestor) + end + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 84930d11a06..6ee19c0ddf4 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -721,6 +721,7 @@ describe Namespace do deep_nested_group = create(:group, parent: nested_group) very_deep_nested_group = create(:group, parent: deep_nested_group) + expect(root_group.root_ancestor).to eq(root_group) expect(nested_group.root_ancestor).to eq(root_group) expect(deep_nested_group.root_ancestor).to eq(root_group) expect(very_deep_nested_group.root_ancestor).to eq(root_group) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 68dcb8b3e22..21c81752301 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -412,6 +412,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 @@ -2129,6 +2131,29 @@ describe Project do 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 let(:project) { create(:project) } -- cgit v1.2.3 From 6c642c087bef3b7925ca5c8cf93078b58a8efe98 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Tue, 4 Dec 2018 00:38:20 +1300 Subject: Eager load clusters to prevent N+1 This also means we need to apply the `current_scope` otherwise this method will return all clusters associated with the groups regardless of any scopes applied to this method --- spec/models/clusters/cluster_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'spec/models') diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index adb0ea3a444..840f74c9890 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -292,6 +292,22 @@ describe Clusters::Cluster 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 } -- cgit v1.2.3