Commit a1d9a0e3 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch '34758-list-ancestor-clusters' into 'master'

Show clusters of ancestors in cluster list page

Closes #34758

See merge request gitlab-org/gitlab-ce!22996
parents dac38419 f82c9dbe
...@@ -18,8 +18,20 @@ class Clusters::ClustersController < Clusters::BaseController ...@@ -18,8 +18,20 @@ class Clusters::ClustersController < Clusters::BaseController
STATUS_POLLING_INTERVAL = 10_000 STATUS_POLLING_INTERVAL = 10_000
def index def index
clusters = ClustersFinder.new(clusterable, current_user, :all).execute finder = ClusterAncestorsFinder.new(clusterable.subject, current_user)
@clusters = clusters.page(params[:page]).per(20) clusters = finder.execute
# Note: We are paginating through an array here but this should OK as:
#
# In CE, we can have a maximum group nesting depth of 21, so including
# project cluster, we can have max 22 clusters for a group hierachy.
# In EE (Premium) we can have any number, as multiple clusters are
# supported, but the number of clusters are fairly low currently.
#
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/55260 also.
@clusters = Kaminari.paginate_array(clusters).page(params[:page]).per(20)
@has_ancestor_clusters = finder.has_ancestor_clusters?
end end
def new def new
......
# frozen_string_literal: true
class ClusterAncestorsFinder
include Gitlab::Utils::StrongMemoize
def initialize(clusterable, current_user)
@clusterable = clusterable
@current_user = current_user
end
def execute
return [] unless can_read_clusters?
clusterable.clusters + ancestor_clusters
end
def has_ancestor_clusters?
ancestor_clusters.any?
end
private
attr_reader :clusterable, :current_user
def can_read_clusters?
Ability.allowed?(current_user, :read_cluster, clusterable)
end
# This unfortunately returns an Array, not a Relation!
def ancestor_clusters
strong_memoize(:ancestor_clusters) do
Clusters::Cluster.ancestor_clusters_for_clusterable(clusterable)
end
end
end
...@@ -2,8 +2,22 @@ ...@@ -2,8 +2,22 @@
module Clusters module Clusters
class ClusterPresenter < Gitlab::View::Presenter::Delegated class ClusterPresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::SanitizeHelper
include ActionView::Helpers::UrlHelper
include IconsHelper
presents :cluster presents :cluster
# We do not want to show the group path for clusters belonging to the
# clusterable, only for the ancestor clusters.
def item_link(clusterable_presenter)
if cluster.group_type? && clusterable != clusterable_presenter.subject
contracted_group_name(cluster.group) + ' / ' + link_to_cluster
else
link_to_cluster
end
end
def gke_cluster_url def gke_cluster_url
"https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp? "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end end
...@@ -12,6 +26,10 @@ module Clusters ...@@ -12,6 +26,10 @@ module Clusters
can?(current_user, :update_cluster, cluster) && created? can?(current_user, :update_cluster, cluster) && created?
end end
def can_read_cluster?
can?(current_user, :read_cluster, cluster)
end
def cluster_type_description def cluster_type_description
if cluster.project_type? if cluster.project_type?
s_("ClusterIntegration|Project cluster") s_("ClusterIntegration|Project cluster")
...@@ -29,5 +47,29 @@ module Clusters ...@@ -29,5 +47,29 @@ module Clusters
raise NotImplementedError raise NotImplementedError
end end
end end
private
def clusterable
if cluster.group_type?
cluster.group
elsif cluster.project_type?
cluster.project
end
end
def contracted_group_name(group)
sanitize(group.full_name)
.sub(%r{\/.*\/}, "/ #{contracted_icon} /")
.html_safe
end
def contracted_icon
sprite_icon('ellipsis_h', size: 12, css_class: 'vertical-align-middle')
end
def link_to_cluster
link_to_if(can_read_cluster?, cluster.name, show_path)
end
end end
end end
-# This partial is overridden in EE -# This partial is overridden in EE
.nav-controls .nav-controls
%span.btn.btn-add-cluster.disabled.js-add-cluster - if clusterable.can_create_cluster? && clusterable.clusters.empty?
= s_("ClusterIntegration|Add Kubernetes cluster") = link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success js-add-cluster'
- else
%span.btn.btn-add-cluster.disabled.js-add-cluster
= s_("ClusterIntegration|Add Kubernetes cluster")
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.table-section.section-60 .table-section.section-60
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster") .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
.table-mobile-content .table-mobile-content
= link_to cluster.name, cluster.show_path = cluster.item_link(clusterable)
- unless cluster.enabled? - unless cluster.enabled?
%span.badge.badge-danger Connection disabled %span.badge.badge-danger Connection disabled
.table-section.section-25 .table-section.section-25
......
...@@ -11,6 +11,13 @@ ...@@ -11,6 +11,13 @@
.nav-text .nav-text
= s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project") = s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project")
= render 'clusters/clusters/buttons' = render 'clusters/clusters/buttons'
- if @has_ancestor_clusters
.bs-callout.bs-callout-info
= s_("ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters.")
%strong
= link_to _('More information'), help_page_path('user/group/clusters/', anchor: 'cluster-precedence')
.clusters-table.js-clusters-list .clusters-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" } .gl-responsive-table-row.table-row-header{ role: "row" }
.table-section.section-60{ role: "rowheader" } .table-section.section-60{ role: "rowheader" }
......
---
title: Show clusters of ancestors in cluster list page
merge_request: 22996
author:
type: changed
...@@ -1497,6 +1497,9 @@ msgstr "" ...@@ -1497,6 +1497,9 @@ msgstr ""
msgid "ClusterIntegration|Choose which of your environments will use this cluster." msgid "ClusterIntegration|Choose which of your environments will use this cluster."
msgstr "" msgstr ""
msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters."
msgstr ""
msgid "ClusterIntegration|Copy API URL" msgid "ClusterIntegration|Copy API URL"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
describe ClusterAncestorsFinder, '#execute' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:user) { create(:user) }
let!(:project_cluster) do
create(:cluster, :provided_by_user, cluster_type: :project_type, projects: [project])
end
let!(:group_cluster) do
create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group])
end
subject { described_class.new(clusterable, user).execute }
context 'for a project' do
let(:clusterable) { project }
before do
project.add_maintainer(user)
end
it 'returns the project clusters followed by group clusters' do
is_expected.to eq([project_cluster, group_cluster])
end
context 'nested groups', :nested_groups do
let(:group) { create(:group, parent: parent_group) }
let(:parent_group) { create(:group) }
let!(:parent_group_cluster) do
create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [parent_group])
end
it 'returns the project clusters followed by group clusters ordered ascending the hierarchy' do
is_expected.to eq([project_cluster, group_cluster, parent_group_cluster])
end
end
end
context 'user cannot read clusters for clusterable' do
let(:clusterable) { project }
it 'returns nothing' do
is_expected.to be_empty
end
end
context 'for a group' do
let(:clusterable) { group }
before do
group.add_maintainer(user)
end
it 'returns the list of group clusters' do
is_expected.to eq([group_cluster])
end
context 'nested groups', :nested_groups do
let(:group) { create(:group, parent: parent_group) }
let(:parent_group) { create(:group) }
let!(:parent_group_cluster) do
create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [parent_group])
end
it 'returns the list of group clusters ordered ascending the hierarchy' do
is_expected.to eq([group_cluster, parent_group_cluster])
end
end
end
end
...@@ -4,9 +4,10 @@ describe Clusters::ClusterPresenter do ...@@ -4,9 +4,10 @@ describe Clusters::ClusterPresenter do
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
let(:cluster) { create(:cluster, :provided_by_gcp, :project) } let(:cluster) { create(:cluster, :provided_by_gcp, :project) }
let(:user) { create(:user) }
subject(:presenter) do subject(:presenter) do
described_class.new(cluster) described_class.new(cluster, current_user: user)
end end
it 'inherits from Gitlab::View::Presenter::Delegated' do it 'inherits from Gitlab::View::Presenter::Delegated' do
...@@ -27,6 +28,129 @@ describe Clusters::ClusterPresenter do ...@@ -27,6 +28,129 @@ describe Clusters::ClusterPresenter do
end end
end end
describe '#item_link' do
let(:clusterable_presenter) { double('ClusterablePresenter', subject: clusterable) }
subject { presenter.item_link(clusterable_presenter) }
context 'for a group cluster' do
let(:cluster) { create(:cluster, cluster_type: :group_type, groups: [group]) }
let(:group) { create(:group, name: 'Foo') }
let(:cluster_link) { "<a href=\"#{group_cluster_path(cluster.group, cluster)}\">#{cluster.name}</a>" }
before do
group.add_maintainer(user)
end
shared_examples 'ancestor clusters' do
context 'ancestor clusters', :nested_groups do
let(:root_group) { create(:group, name: 'Root Group') }
let(:parent) { create(:group, name: 'parent', parent: root_group) }
let(:child) { create(:group, name: 'child', parent: parent) }
let(:group) { create(:group, name: 'group', parent: child) }
before do
root_group.add_maintainer(user)
end
context 'top level group cluster' do
let(:cluster) { create(:cluster, cluster_type: :group_type, groups: [root_group]) }
it 'returns full group names and link for cluster' do
expect(subject).to eq("Root Group / #{cluster_link}")
end
it 'is html safe' do
expect(presenter).to receive(:sanitize).with('Root Group').and_call_original
expect(subject).to be_html_safe
end
end
context 'first level group cluster' do
let(:cluster) { create(:cluster, cluster_type: :group_type, groups: [parent]) }
it 'returns full group names and link for cluster' do
expect(subject).to eq("Root Group / parent / #{cluster_link}")
end
it 'is html safe' do
expect(presenter).to receive(:sanitize).with('Root Group / parent').and_call_original
expect(subject).to be_html_safe
end
end
context 'second level group cluster' do
let(:cluster) { create(:cluster, cluster_type: :group_type, groups: [child]) }
let(:ellipsis_h) do
/.*ellipsis_h.*/
end
it 'returns clipped group names and link for cluster' do
expect(subject).to match("Root Group / #{ellipsis_h} / child / #{cluster_link}")
end
it 'is html safe' do
expect(presenter).to receive(:sanitize).with('Root Group / parent / child').and_call_original
expect(subject).to be_html_safe
end
end
end
end
context 'for a project clusterable' do
let(:clusterable) { project }
let(:project) { create(:project, group: group) }
it 'returns the group name and the link for cluster' do
expect(subject).to eq("Foo / #{cluster_link}")
end
it 'is html safe' do
expect(presenter).to receive(:sanitize).with('Foo').and_call_original
expect(subject).to be_html_safe
end
include_examples 'ancestor clusters'
end
context 'for the group clusterable for the cluster' do
let(:clusterable) { group }
it 'returns link for cluster' do
expect(subject).to eq(cluster_link)
end
include_examples 'ancestor clusters'
it 'is html safe' do
expect(subject).to be_html_safe
end
end
end
context 'for a project cluster' do
let(:cluster) { create(:cluster, :project) }
let(:cluster_link) { "<a href=\"#{project_cluster_path(cluster.project, cluster)}\">#{cluster.name}</a>" }
before do
cluster.project.add_maintainer(user)
end
context 'for the project clusterable' do
let(:clusterable) { cluster.project }
it 'returns link for cluster' do
expect(subject).to eq(cluster_link)
end
end
end
end
describe '#gke_cluster_url' do describe '#gke_cluster_url' do
subject { described_class.new(cluster).gke_cluster_url } subject { described_class.new(cluster).gke_cluster_url }
......
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