diff options
author | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2018-12-17 10:25:19 +0000 |
---|---|---|
committer | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2018-12-17 10:25:19 +0000 |
commit | a1d9a0e34c3404d22a8d1ffebc5f50136630d24a (patch) | |
tree | 730ab2a205d38e5d0dd147ba03c4e37e45b90a03 | |
parent | dac38419fe723ae9e327afe1fa5aab4c61aebb98 (diff) | |
parent | f82c9dbe44d5d003dbeb084f07615ba26c2294b6 (diff) | |
download | gitlab-ce-a1d9a0e34c3404d22a8d1ffebc5f50136630d24a.tar.gz |
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
-rw-r--r-- | app/controllers/clusters/clusters_controller.rb | 16 | ||||
-rw-r--r-- | app/finders/cluster_ancestors_finder.rb | 35 | ||||
-rw-r--r-- | app/presenters/clusters/cluster_presenter.rb | 42 | ||||
-rw-r--r-- | app/views/clusters/clusters/_buttons.html.haml | 7 | ||||
-rw-r--r-- | app/views/clusters/clusters/_cluster.html.haml | 2 | ||||
-rw-r--r-- | app/views/clusters/clusters/index.html.haml | 7 | ||||
-rw-r--r-- | changelogs/unreleased/34758-list-ancestor-clusters.yml | 5 | ||||
-rw-r--r-- | locale/gitlab.pot | 3 | ||||
-rw-r--r-- | spec/finders/cluster_ancestors_finder_spec.rb | 77 | ||||
-rw-r--r-- | spec/presenters/clusters/cluster_presenter_spec.rb | 126 |
10 files changed, 314 insertions, 6 deletions
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index 9aa8b758539..b9717b97640 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -18,8 +18,20 @@ class Clusters::ClustersController < Clusters::BaseController STATUS_POLLING_INTERVAL = 10_000 def index - clusters = ClustersFinder.new(clusterable, current_user, :all).execute - @clusters = clusters.page(params[:page]).per(20) + finder = ClusterAncestorsFinder.new(clusterable.subject, current_user) + 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 def new diff --git a/app/finders/cluster_ancestors_finder.rb b/app/finders/cluster_ancestors_finder.rb new file mode 100644 index 00000000000..2f9709ee057 --- /dev/null +++ b/app/finders/cluster_ancestors_finder.rb @@ -0,0 +1,35 @@ +# 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 diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb index 0473bb8c72a..7a5b68f9a4b 100644 --- a/app/presenters/clusters/cluster_presenter.rb +++ b/app/presenters/clusters/cluster_presenter.rb @@ -2,8 +2,22 @@ module Clusters class ClusterPresenter < Gitlab::View::Presenter::Delegated + include ActionView::Helpers::SanitizeHelper + include ActionView::Helpers::UrlHelper + include IconsHelper + 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 "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp? end @@ -12,6 +26,10 @@ module Clusters can?(current_user, :update_cluster, cluster) && created? end + def can_read_cluster? + can?(current_user, :read_cluster, cluster) + end + def cluster_type_description if cluster.project_type? s_("ClusterIntegration|Project cluster") @@ -29,5 +47,29 @@ module Clusters raise NotImplementedError 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 diff --git a/app/views/clusters/clusters/_buttons.html.haml b/app/views/clusters/clusters/_buttons.html.haml index db2e247e341..9238903aa10 100644 --- a/app/views/clusters/clusters/_buttons.html.haml +++ b/app/views/clusters/clusters/_buttons.html.haml @@ -1,4 +1,7 @@ -# This partial is overridden in EE .nav-controls - %span.btn.btn-add-cluster.disabled.js-add-cluster - = s_("ClusterIntegration|Add Kubernetes cluster") + - if clusterable.can_create_cluster? && clusterable.clusters.empty? + = 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") diff --git a/app/views/clusters/clusters/_cluster.html.haml b/app/views/clusters/clusters/_cluster.html.haml index e15de3d504d..b89789e9915 100644 --- a/app/views/clusters/clusters/_cluster.html.haml +++ b/app/views/clusters/clusters/_cluster.html.haml @@ -3,7 +3,7 @@ .table-section.section-60 .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster") .table-mobile-content - = link_to cluster.name, cluster.show_path + = cluster.item_link(clusterable) - unless cluster.enabled? %span.badge.badge-danger Connection disabled .table-section.section-25 diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml index ad6d1d856d6..58d0a304363 100644 --- a/app/views/clusters/clusters/index.html.haml +++ b/app/views/clusters/clusters/index.html.haml @@ -11,6 +11,13 @@ .nav-text = s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project") = 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 .gl-responsive-table-row.table-row-header{ role: "row" } .table-section.section-60{ role: "rowheader" } diff --git a/changelogs/unreleased/34758-list-ancestor-clusters.yml b/changelogs/unreleased/34758-list-ancestor-clusters.yml new file mode 100644 index 00000000000..8fdba7ba90a --- /dev/null +++ b/changelogs/unreleased/34758-list-ancestor-clusters.yml @@ -0,0 +1,5 @@ +--- +title: Show clusters of ancestors in cluster list page +merge_request: 22996 +author: +type: changed diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0584d2006f7..ef1c56a50ab 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1497,6 +1497,9 @@ msgstr "" msgid "ClusterIntegration|Choose which of your environments will use this cluster." 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" msgstr "" diff --git a/spec/finders/cluster_ancestors_finder_spec.rb b/spec/finders/cluster_ancestors_finder_spec.rb new file mode 100644 index 00000000000..332086c42e2 --- /dev/null +++ b/spec/finders/cluster_ancestors_finder_spec.rb @@ -0,0 +1,77 @@ +# 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 diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb index 63c2ff26a45..754ba0a594c 100644 --- a/spec/presenters/clusters/cluster_presenter_spec.rb +++ b/spec/presenters/clusters/cluster_presenter_spec.rb @@ -4,9 +4,10 @@ describe Clusters::ClusterPresenter do include Gitlab::Routing.url_helpers let(:cluster) { create(:cluster, :provided_by_gcp, :project) } + let(:user) { create(:user) } subject(:presenter) do - described_class.new(cluster) + described_class.new(cluster, current_user: user) end it 'inherits from Gitlab::View::Presenter::Delegated' do @@ -27,6 +28,129 @@ describe Clusters::ClusterPresenter do 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 subject { described_class.new(cluster).gke_cluster_url } |