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 @@
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
......
# 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
# 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
# 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
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.
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
......
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 }