Commit 8b1212ed authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '34758-group-cluster-controller' into 'master'

User can create a group level cluster and install applications

See merge request gitlab-org/gitlab-ce!22450
parents aa958aba b054694e
...@@ -9,7 +9,7 @@ import eventHub from './event_hub'; ...@@ -9,7 +9,7 @@ import eventHub from './event_hub';
import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants'; import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store'; import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue'; import Applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons'; import setupToggleButtons from '../toggle_buttons';
/** /**
...@@ -31,6 +31,7 @@ export default class Clusters { ...@@ -31,6 +31,7 @@ export default class Clusters {
installKnativePath, installKnativePath,
installPrometheusPath, installPrometheusPath,
managePrometheusPath, managePrometheusPath,
clusterType,
clusterStatus, clusterStatus,
clusterStatusReason, clusterStatusReason,
helpPath, helpPath,
...@@ -67,7 +68,7 @@ export default class Clusters { ...@@ -67,7 +68,7 @@ export default class Clusters {
initDismissableCallout('.js-cluster-security-warning'); initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels(); initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications(); this.initApplications(clusterType);
if (this.store.state.status !== 'created') { if (this.store.state.status !== 'created') {
this.updateContainer(null, this.store.state.status, this.store.state.statusReason); this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
...@@ -79,23 +80,21 @@ export default class Clusters { ...@@ -79,23 +80,21 @@ export default class Clusters {
} }
} }
initApplications() { initApplications(type) {
const { store } = this; const { store } = this;
const el = document.querySelector('#js-cluster-applications'); const el = document.querySelector('#js-cluster-applications');
this.applications = new Vue({ this.applications = new Vue({
el, el,
components: {
applications,
},
data() { data() {
return { return {
state: store.state, state: store.state,
}; };
}, },
render(createElement) { render(createElement) {
return createElement('applications', { return createElement(Applications, {
props: { props: {
type,
applications: this.state.applications, applications: this.state.applications,
helpPath: this.state.helpPath, helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath, ingressHelpPath: this.state.ingressHelpPath,
......
import createFlash from '~/flash';
import { __ } from '~/locale';
import setupToggleButtons from '~/toggle_buttons';
import initDismissableCallout from '~/dismissable_callout';
import ClustersService from './services/clusters_service';
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
initDismissableCallout('.gcp-signup-offer');
// The empty state won't have a clusterList
if (clusterList) {
setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
err => {
createFlash(__('Something went wrong on our end.'));
throw err;
},
),
);
}
};
...@@ -13,7 +13,7 @@ import prometheusLogo from 'images/cluster_app_logos/prometheus.png'; ...@@ -13,7 +13,7 @@ import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue'; import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { APPLICATION_STATUS, INGRESS } from '../constants'; import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
export default { export default {
components: { components: {
...@@ -21,6 +21,11 @@ export default { ...@@ -21,6 +21,11 @@ export default {
clipboardButton, clipboardButton,
}, },
props: { props: {
type: {
type: String,
required: false,
default: CLUSTER_TYPE.PROJECT,
},
applications: { applications: {
type: Object, type: Object,
required: false, required: false,
...@@ -59,6 +64,9 @@ export default { ...@@ -59,6 +64,9 @@ export default {
prometheusLogo, prometheusLogo,
}), }),
computed: { computed: {
isProjectCluster() {
return this.type === CLUSTER_TYPE.PROJECT;
},
helmInstalled() { helmInstalled() {
return ( return (
this.applications.helm.status === APPLICATION_STATUS.INSTALLED || this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
...@@ -281,6 +289,7 @@ export default { ...@@ -281,6 +289,7 @@ export default {
</div> </div>
</application-row> </application-row>
<application-row <application-row
v-if="isProjectCluster"
id="prometheus" id="prometheus"
:logo-url="prometheusLogo" :logo-url="prometheusLogo"
:title="applications.prometheus.title" :title="applications.prometheus.title"
...@@ -299,6 +308,7 @@ export default { ...@@ -299,6 +308,7 @@ export default {
</div> </div>
</application-row> </application-row>
<application-row <application-row
v-if="isProjectCluster"
id="runner" id="runner"
:logo-url="gitlabLogo" :logo-url="gitlabLogo"
:title="applications.runner.title" :title="applications.runner.title"
...@@ -317,6 +327,7 @@ export default { ...@@ -317,6 +327,7 @@ export default {
</div> </div>
</application-row> </application-row>
<application-row <application-row
v-if="isProjectCluster"
id="jupyter" id="jupyter"
:logo-url="jupyterhubLogo" :logo-url="jupyterhubLogo"
:title="applications.jupyter.title" :title="applications.jupyter.title"
......
// These need to match the enum found in app/models/clusters/cluster.rb
export const CLUSTER_TYPE = {
INSTANCE: 'instance_type',
GROUP: 'group_type',
PROJECT: 'project_type',
};
// These need to match what is returned from the server // These need to match what is returned from the server
export const APPLICATION_STATUS = { export const APPLICATION_STATUS = {
NOT_INSTALLABLE: 'not_installable', NOT_INSTALLABLE: 'not_installable',
......
import ClustersBundle from '~/clusters/clusters_bundle';
document.addEventListener('DOMContentLoaded', () => {
new ClustersBundle(); // eslint-disable-line no-new
});
import initDismissableCallout from '~/dismissable_callout';
document.addEventListener('DOMContentLoaded', () => {
initDismissableCallout('.gcp-signup-offer');
});
import ClustersBundle from '~/clusters/clusters_bundle';
document.addEventListener('DOMContentLoaded', () => {
new ClustersBundle(); // eslint-disable-line no-new
});
import ClustersBundle from '~/clusters/clusters_bundle';
document.addEventListener('DOMContentLoaded', () => {
new ClustersBundle(); // eslint-disable-line no-new
});
import initDismissableCallout from '~/dismissable_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
document.addEventListener('DOMContentLoaded', () => {
const { page } = document.body.dataset;
const newClusterViews = [
'groups:clusters:new',
'groups:clusters:create_gcp',
'groups:clusters:create_user',
];
if (newClusterViews.indexOf(page) > -1) {
initDismissableCallout('.gcp-signup-offer');
initGkeDropdowns();
}
});
import ClustersIndex from '~/clusters/clusters_index'; import initDismissableCallout from '~/dismissable_callout';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new ClustersIndex(); // eslint-disable-line no-new initDismissableCallout('.gcp-signup-offer');
}); });
...@@ -25,6 +25,12 @@ ...@@ -25,6 +25,12 @@
.cluster-application-row { .cluster-application-row {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
padding: $gl-padding; padding: $gl-padding;
&:last-child {
border-bottom: 0;
border-bottom-left-radius: calc(#{$border-radius-default} - 1px);
border-bottom-right-radius: calc(#{$border-radius-default} - 1px);
}
} }
} }
...@@ -73,6 +79,10 @@ ...@@ -73,6 +79,10 @@
padding: $gl-padding-top $gl-padding; padding: $gl-padding-top $gl-padding;
} }
.card {
margin-bottom: $gl-vert-padding;
}
.empty-state .svg-content img { .empty-state .svg-content img {
width: 145px; width: 145px;
} }
...@@ -80,6 +90,31 @@ ...@@ -80,6 +90,31 @@
.top-area .nav-controls > .btn.btn-add-cluster { .top-area .nav-controls > .btn.btn-add-cluster {
margin-right: 0; margin-right: 0;
} }
.clusters-table {
background-color: $gray-light;
padding: $gl-padding-8;
}
.badge-light {
background-color: $white-normal;
}
.gl-responsive-table-row {
padding: $gl-padding;
border: 0;
&.table-row-header {
background-color: none;
border: 0;
font-weight: bold;
color: $gl-gray-500;
}
}
}
.cluster-warning {
@include alert-variant(theme-color-level('warning', $alert-bg-level), theme-color-level('warning', $alert-border-level), theme-color-level('warning', $alert-color-level));
} }
.gcp-signup-offer { .gcp-signup-offer {
......
...@@ -183,13 +183,13 @@ class Clusters::ClustersController < Clusters::BaseController ...@@ -183,13 +183,13 @@ class Clusters::ClustersController < Clusters::BaseController
def gcp_cluster def gcp_cluster
@gcp_cluster = ::Clusters::Cluster.new.tap do |cluster| @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp cluster.build_provider_gcp
end end.present(current_user: current_user)
end end
def user_cluster def user_cluster
@user_cluster = ::Clusters::Cluster.new.tap do |cluster| @user_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_platform_kubernetes cluster.build_platform_kubernetes
end end.present(current_user: current_user)
end end
def validate_gcp_token def validate_gcp_token
......
# frozen_string_literal: true
class Groups::Clusters::ApplicationsController < Clusters::ApplicationsController
include ControllerWithCrossProjectAccessCheck
prepend_before_action :group
requires_cross_project_access
private
def clusterable
@clusterable ||= ClusterablePresenter.fabricate(group, current_user: current_user)
end
def group
@group ||= find_routable!(Group, params[:group_id] || params[:id])
end
end
# frozen_string_literal: true
class Groups::ClustersController < Clusters::ClustersController
include ControllerWithCrossProjectAccessCheck
prepend_before_action :check_group_clusters_feature_flag!
prepend_before_action :group
requires_cross_project_access
layout 'group'
private
def clusterable
@clusterable ||= ClusterablePresenter.fabricate(group, current_user: current_user)
end
def group
@group ||= find_routable!(Group, params[:group_id] || params[:id])
end
def check_group_clusters_feature_flag!
render_404 unless Feature.enabled?(:group_clusters)
end
end
...@@ -140,6 +140,10 @@ module GroupsHelper ...@@ -140,6 +140,10 @@ module GroupsHelper
can?(current_user, "read_group_#{resource}".to_sym, @group) can?(current_user, "read_group_#{resource}".to_sym, @group)
end end
if can?(current_user, :read_cluster, @group) && Feature.enabled?(:group_clusters)
links << :kubernetes
end
if can?(current_user, :admin_group, @group) if can?(current_user, :admin_group, @group)
links << :settings links << :settings
end end
......
...@@ -29,7 +29,7 @@ module Clusters ...@@ -29,7 +29,7 @@ module Clusters
# we force autosave to happen when we save `Cluster` model # we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true
has_one :application_helm, class_name: 'Clusters::Applications::Helm' has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress' has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
...@@ -144,6 +144,10 @@ module Clusters ...@@ -144,6 +144,10 @@ module Clusters
) )
end end
def allow_user_defined_namespace?
project_type?
end
private private
def restrict_modification def restrict_modification
......
...@@ -38,6 +38,8 @@ module Clusters ...@@ -38,6 +38,8 @@ module Clusters
validates :namespace, exclusion: { in: RESERVED_NAMESPACES } validates :namespace, exclusion: { in: RESERVED_NAMESPACES }
validate :no_namespace, unless: :allow_user_defined_namespace?
# We expect to be `active?` only when enabled and cluster is created (the api_url is assigned) # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
validates :api_url, url: true, presence: true validates :api_url, url: true, presence: true
validates :token, presence: true validates :token, presence: true
...@@ -52,6 +54,7 @@ module Clusters ...@@ -52,6 +54,7 @@ module Clusters
delegate :project, to: :cluster, allow_nil: true delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, to: :cluster, allow_nil: true delegate :enabled?, to: :cluster, allow_nil: true
delegate :managed?, to: :cluster, allow_nil: true delegate :managed?, to: :cluster, allow_nil: true
delegate :allow_user_defined_namespace?, to: :cluster, allow_nil: true
delegate :kubernetes_namespace, to: :cluster delegate :kubernetes_namespace, to: :cluster
alias_method :active?, :enabled? alias_method :active?, :enabled?
...@@ -150,7 +153,8 @@ module Clusters ...@@ -150,7 +153,8 @@ module Clusters
end end
def build_kube_client! def build_kube_client!
raise "Incomplete settings" unless api_url && actual_namespace raise "Incomplete settings" unless api_url
raise "No namespace" if cluster.project_type? && actual_namespace.empty? # can probably remove this line once we remove #actual_namespace
unless (username && password) || token unless (username && password) || token
raise "Either username/password or token is required to access API" raise "Either username/password or token is required to access API"
...@@ -207,6 +211,12 @@ module Clusters ...@@ -207,6 +211,12 @@ module Clusters
self.token = self.token&.strip self.token = self.token&.strip
end end
def no_namespace
if namespace
errors.add(:namespace, 'only allowed for project cluster')
end
end
def prevent_modification def prevent_modification
return unless managed? return unless managed?
......
...@@ -4,11 +4,7 @@ module Clusters ...@@ -4,11 +4,7 @@ module Clusters
class ClusterPolicy < BasePolicy class ClusterPolicy < BasePolicy
alias_method :cluster, :subject alias_method :cluster, :subject
delegate { cluster.group }
delegate { cluster.project } delegate { cluster.project }
rule { can?(:maintainer_access) }.policy do
enable :update_cluster
enable :admin_cluster
end
end end
end end
...@@ -65,6 +65,10 @@ class GroupPolicy < BasePolicy ...@@ -65,6 +65,10 @@ class GroupPolicy < BasePolicy
enable :create_projects enable :create_projects
enable :admin_pipeline enable :admin_pipeline
enable :admin_build enable :admin_build
enable :read_cluster
enable :create_cluster
enable :update_cluster
enable :admin_cluster
end end
rule { owner }.policy do rule { owner }.policy do
......
...@@ -258,6 +258,8 @@ class ProjectPolicy < BasePolicy ...@@ -258,6 +258,8 @@ class ProjectPolicy < BasePolicy
enable :update_pages enable :update_pages
enable :read_cluster enable :read_cluster
enable :create_cluster enable :create_cluster
enable :update_cluster
enable :admin_cluster
enable :create_environment_terminal enable :create_environment_terminal
end end
......
...@@ -43,4 +43,16 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated ...@@ -43,4 +43,16 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
def cluster_path(cluster, params = {}) def cluster_path(cluster, params = {})
raise NotImplementedError raise NotImplementedError
end end
def empty_state_help_text
nil
end
def sidebar_text
raise NotImplementedError
end
def learn_more_link
raise NotImplementedError
end
end end
...@@ -15,6 +15,8 @@ module Clusters ...@@ -15,6 +15,8 @@ module Clusters
def show_path def show_path
if cluster.project_type? if cluster.project_type?
project_cluster_path(project, cluster) project_cluster_path(project, cluster)
elsif cluster.group_type?
group_cluster_path(group, cluster)
else else
raise NotImplementedError raise NotImplementedError
end end
......
# frozen_string_literal: true
class GroupClusterablePresenter < ClusterablePresenter
extend ::Gitlab::Utils::Override
include ActionView::Helpers::UrlHelper
override :cluster_status_cluster_path
def cluster_status_cluster_path(cluster, params = {})
cluster_status_group_cluster_path(clusterable, cluster, params)
end
override :install_applications_cluster_path
def install_applications_cluster_path(cluster, application)
install_applications_group_cluster_path(clusterable, cluster, application)
end
override :cluster_path
def cluster_path(cluster, params = {})
group_cluster_path(clusterable, cluster, params)
end
override :empty_state_help_text
def empty_state_help_text
s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.')
end
override :sidebar_text
def sidebar_text
s_('ClusterIntegration|Adding a Kubernetes cluster to your group will automatically share the cluster across all your projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster.')
end
override :learn_more_link
def learn_more_link
link_to(s_('ClusterIntegration|Learn more about group Kubernetes clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
end
end
# frozen_string_literal: true # frozen_string_literal: true
class ProjectClusterablePresenter < ClusterablePresenter class ProjectClusterablePresenter < ClusterablePresenter
extend ::Gitlab::Utils::Override
include ActionView::Helpers::UrlHelper
override :cluster_status_cluster_path
def cluster_status_cluster_path(cluster, params = {}) def cluster_status_cluster_path(cluster, params = {})
cluster_status_project_cluster_path(clusterable, cluster, params) cluster_status_project_cluster_path(clusterable, cluster, params)
end end
override :install_applications_cluster_path
def install_applications_cluster_path(cluster, application) def install_applications_cluster_path(cluster, application)
install_applications_project_cluster_path(clusterable, cluster, application) install_applications_project_cluster_path(clusterable, cluster, application)
end end
override :cluster_path
def cluster_path(cluster, params = {}) def cluster_path(cluster, params = {})
project_cluster_path(clusterable, cluster, params) project_cluster_path(clusterable, cluster, params)
end end
override :sidebar_text
def sidebar_text
s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
end
override :learn_more_link
def learn_more_link
link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
end
end end
...@@ -42,7 +42,16 @@ module Clusters ...@@ -42,7 +42,16 @@ module Clusters
def builders def builders
{ {
"helm" => -> (cluster) { cluster.application_helm || cluster.build_application_helm }, "helm" => -> (cluster) { cluster.application_helm || cluster.build_application_helm },
"ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress }, "ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress }
}.tap do |hash|
hash.merge!(project_builders) if cluster.project_type?
end
end
# These applications will need extra configuration to enable them to work
# with groups of projects
def project_builders
{
"prometheus" => -> (cluster) { cluster.application_prometheus || cluster.build_application_prometheus }, "prometheus" => -> (cluster) { cluster.application_prometheus || cluster.build_application_prometheus },
"runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner }, "runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner },
"jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter }, "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter },
......
...@@ -36,6 +36,10 @@ module Clusters ...@@ -36,6 +36,10 @@ module Clusters
case clusterable case clusterable
when ::Project when ::Project
{ cluster_type: :project_type, projects: [clusterable] } { cluster_type: :project_type, projects: [clusterable] }
when ::Group
{ cluster_type: :group_type, groups: [clusterable] }
else
raise NotImplementedError
end end
end end
......
-# This partial is overridden in EE
.nav-controls
%span.btn.btn-add-cluster.disabled.js-add-cluster
= s_("ClusterIntegration|Add Kubernetes cluster")
.gl-responsive-table-row .card
.table-section.section-30 .card-body.gl-responsive-table-row
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster") .table-section.section-60
.table-mobile-content .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
= link_to cluster.name, cluster.show_path .table-mobile-content
.table-section.section-30 = link_to cluster.name, cluster.show_path
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope") - unless cluster.enabled?
.table-mobile-content= cluster.environment_scope %span.badge.badge-danger Connection disabled
.table-section.section-30 .table-section.section-25
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace") .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
.table-mobile-content= cluster.platform_kubernetes&.actual_namespace .table-mobile-content= cluster.environment_scope
.table-section.section-10 .table-section.section-15.text-right
.table-mobile-header{ role: "rowheader" } .table-mobile-header{ role: "rowheader" }
.table-mobile-content .table-mobile-content
%button.js-project-feature-toggle.project-feature-toggle{ type: "button", %span.badge.badge-light
class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", = cluster.project_type? ? s_("ClusterIntegration|Project cluster") : s_("ClusterIntegration|Group cluster")
"aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"),
disabled: !cluster.can_toggle_cluster?,
data: { endpoint: clusterable.cluster_path(cluster, format: :json) } }
%input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? }
= icon("spinner spin", class: "loading-icon")
%span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
...@@ -4,8 +4,10 @@ ...@@ -4,8 +4,10 @@
.col-12 .col-12
.text-content .text-content
%h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation') %h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation')
- link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') %p
%p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page} = s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
= clusterable.empty_state_help_text
= clusterable.learn_more_link
- if clusterable.can_create_cluster? - if clusterable.can_create_cluster?
.text-center .text-center
......
- clusters_help_url = help_page_path('user/project/clusters/index.md')
- help_link_start = "<a href=\"%{url}\" target=\"_blank\" rel=\"noopener noreferrer\">".html_safe
- help_link_end = '</a>'.html_safe
%h4.prepend-top-0 %h4.prepend-top-0
= s_('ClusterIntegration|Kubernetes cluster integration') = s_('ClusterIntegration|Add a Kubernetes cluster integration')
%p %p
= s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.') = clusterable.sidebar_text
%p %p
= s_('ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: clusters_help_url }, help_link_end: help_link_end } = clusterable.learn_more_link
...@@ -33,9 +33,10 @@ ...@@ -33,9 +33,10 @@
= s_('ClusterIntegration|Show') = s_('ClusterIntegration|Show')
= clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default') = clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
.form-group - if @cluster.allow_user_defined_namespace?
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') .form-group
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group .form-group
.form-check .form-check
......
...@@ -10,14 +10,13 @@ ...@@ -10,14 +10,13 @@
.top-area.adjust .top-area.adjust
.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")
.ci-table.js-clusters-list = render 'clusters/clusters/buttons'
.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-30{ role: "rowheader" } .table-section.section-60{ role: "rowheader" }
= s_("ClusterIntegration|Kubernetes cluster") = s_("ClusterIntegration|Kubernetes cluster")
.table-section.section-30{ role: "rowheader" } .table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Environment scope") = s_("ClusterIntegration|Environment scope")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Project namespace")
.table-section.section-10{ role: "rowheader" } .table-section.section-10{ role: "rowheader" }
- @clusters.each do |cluster| - @clusters.each do |cluster|
= render "cluster", cluster: cluster.present(current_user: current_user) = render "cluster", cluster: cluster.present(current_user: current_user)
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter), install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative), install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
toggle_status: @cluster.enabled? ? 'true': 'false', toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_type: @cluster.cluster_type,
cluster_status: @cluster.status_name, cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason, cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'), help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
......
...@@ -21,9 +21,10 @@ ...@@ -21,9 +21,10 @@
= platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold' = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold'
= platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off' = platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
.form-group - if @user_cluster.allow_user_defined_namespace?
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' .form-group
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group .form-group
.form-check .form-check
......
...@@ -22,9 +22,10 @@ ...@@ -22,9 +22,10 @@
%button.js-show-cluster-token.btn-blank{ type: 'button' } %button.js-show-cluster-token.btn-blank{ type: 'button' }
= s_('ClusterIntegration|Show') = s_('ClusterIntegration|Show')
.form-group - if @cluster.allow_user_defined_namespace?
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' .form-group
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group .form-group
.form-check .form-check
......
...@@ -116,6 +116,19 @@ ...@@ -116,6 +116,19 @@
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('Members') = _('Members')
- if group_sidebar_link?(:kubernetes)
= nav_link(controller: [:clusters]) do
= link_to group_clusters_path(@group) do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
= _('Kubernetes')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do
= link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%strong.fly-out-top-item-name
= _('Kubernetes')
- if group_sidebar_link?(:settings) - if group_sidebar_link?(:settings)
= nav_link(path: group_nav_link_paths) do = nav_link(path: group_nav_link_paths) do
= link_to edit_group_path(@group) do = link_to edit_group_path(@group) do
......
---
title: Add ability to create group level clusters and install gitlab managed applications
merge_request: 22450
author:
type: added
...@@ -53,6 +53,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do ...@@ -53,6 +53,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resource :avatar, only: [:destroy] resource :avatar, only: [:destroy]
concerns :clusterable
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member post :resend_invite, on: :member
delete :leave, on: :collection delete :leave, on: :collection
......
...@@ -1364,6 +1364,15 @@ msgstr "" ...@@ -1364,6 +1364,15 @@ msgstr ""
msgid "ClusterIntegration|Add Kubernetes cluster" msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Add a Kubernetes cluster integration"
msgstr ""
msgid "ClusterIntegration|Adding a Kubernetes cluster to your group will automatically share the cluster across all your projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster."
msgstr ""
msgid "ClusterIntegration|Adding an integration to your group will share the cluster across all your projects."
msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration" msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr "" msgstr ""
...@@ -1466,6 +1475,9 @@ msgstr "" ...@@ -1466,6 +1475,9 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project" msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr "" msgstr ""
msgid "ClusterIntegration|Group cluster"
msgstr ""
msgid "ClusterIntegration|Helm Tiller" msgid "ClusterIntegration|Helm Tiller"
msgstr "" msgstr ""
...@@ -1520,9 +1532,6 @@ msgstr "" ...@@ -1520,9 +1532,6 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes cluster details" msgid "ClusterIntegration|Kubernetes cluster details"
msgstr "" msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..." msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
msgstr "" msgstr ""
...@@ -1532,7 +1541,7 @@ msgstr "" ...@@ -1532,7 +1541,7 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details" msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details"
msgstr "" msgstr ""
msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}" msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "" msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project" msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
...@@ -1541,10 +1550,13 @@ msgstr "" ...@@ -1541,10 +1550,13 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}." msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
msgstr "" msgstr ""
msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}." msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr "" msgstr ""
msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}." msgid "ClusterIntegration|Learn more about Kubernetes"
msgstr ""
msgid "ClusterIntegration|Learn more about group Kubernetes clusters"
msgstr "" msgstr ""
msgid "ClusterIntegration|Machine type" msgid "ClusterIntegration|Machine type"
...@@ -1589,6 +1601,9 @@ msgstr "" ...@@ -1589,6 +1601,9 @@ msgstr ""
msgid "ClusterIntegration|Point a wildcard DNS to this generated IP address in order to access your application after it has been deployed." msgid "ClusterIntegration|Point a wildcard DNS to this generated IP address in order to access your application after it has been deployed."
msgstr "" msgstr ""
msgid "ClusterIntegration|Project cluster"
msgstr ""
msgid "ClusterIntegration|Project namespace" msgid "ClusterIntegration|Project namespace"
msgstr "" msgstr ""
...@@ -1679,9 +1694,6 @@ msgstr "" ...@@ -1679,9 +1694,6 @@ msgstr ""
msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters." msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters."
msgstr "" msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes Cluster"
msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes cluster" msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
describe Groups::Clusters::ApplicationsController do
include AccessMatchersForController
def current_application
Clusters::Cluster::APPLICATIONS[application]
end
describe 'POST create' do
let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
let(:group) { cluster.group }
let(:application) { 'helm' }
let(:params) { { application: application, id: cluster.id } }
describe 'functionality' do
let(:user) { create(:user) }
before do
group.add_maintainer(user)
sign_in(user)
end
it 'schedule an application installation' do
expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
expect { go }.to change { current_application.count }
expect(response).to have_http_status(:no_content)
expect(cluster.application_helm).to be_scheduled
end
context 'when cluster do not exists' do
before do
cluster.destroy!
end
it 'return 404' do
expect { go }.not_to change { current_application.count }
expect(response).to have_http_status(:not_found)
end
end
context 'when application is unknown' do
let(:application) { 'unkwnown-app' }
it 'return 404' do
go
expect(response).to have_http_status(:not_found)
end
end
context 'when application is already installing' do
before do
create(:clusters_applications_helm, :installing, cluster: cluster)
end
it 'returns 400' do
go
expect(response).to have_http_status(:bad_request)
end
end
end
describe 'security' do
before do
allow(ClusterInstallAppWorker).to receive(:perform_async)
end
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
def go
post :create, params.merge(group_id: group)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Groups::ClustersController do
include AccessMatchersForController
include GoogleApi::CloudPlatformHelpers
set(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_maintainer(user)
sign_in(user)
end
describe 'GET index' do
def go(params = {})
get :index, params.reverse_merge(group_id: group)
end
context 'when feature flag is not enabled' do
before do
stub_feature_flags(group_clusters: false)
end
it 'renders 404' do
go
expect(response).to have_gitlab_http_status(404)
end
end
context 'when feature flag is enabled' do
before do
stub_feature_flags(group_clusters: true)
end
describe 'functionality' do
context 'when group has one or more clusters' do
let(:group) { create(:group) }
let!(:enabled_cluster) do
create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group])
end
let!(:disabled_cluster) do
create(:cluster, :disabled, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
end
it 'lists available clusters' do
go
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
end
context 'when page is specified' do
let(:last_page) { group.clusters.page.total_pages }
before do
allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
create_list(:cluster, 2, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
end
it 'redirects to the page' do
go(page: last_page)
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:clusters).current_page).to eq(last_page)
end
end
end
context 'when group does not have a cluster' do
let(:group) { create(:group) }
it 'returns an empty state page' do
go
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index, partial: :empty_state)
expect(assigns(:clusters)).to eq([])
end
end
end
end
describe 'security' do
let(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
end
describe 'GET new' do
def go
get :new, group_id: group
end
describe 'functionality for new cluster' do
context 'when omniauth has been configured' do
let(:key) { 'secret-key' }
let(:session_key_for_redirect_uri) do
GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
end
before do
allow(SecureRandom).to receive(:hex).and_return(key)
end
it 'has authorize_url' do
go
expect(assigns(:authorize_url)).to include(key)
expect(session[session_key_for_redirect_uri]).to eq(new_group_cluster_path(group))
end
end
context 'when omniauth has not configured' do
before do
stub_omniauth_setting(providers: [])
end
it 'does not have authorize_url' do
go
expect(assigns(:authorize_url)).to be_nil
end
end
context 'when access token is valid' do
before do
stub_google_api_validate_token
end
it 'has new object' do
go
expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
end
end
context 'when access token is expired' do
before do
stub_google_api_expired_token
end
it { expect(@valid_gcp_token).to be_falsey }
end
context 'when access token is not stored in session' do
it { expect(@valid_gcp_token).to be_falsey }
end
end
describe 'functionality for existing cluster' do
it 'has new object' do
go
expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
end
end
describe 'security' do
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
end
describe 'POST create for new cluster' do
let(:legacy_abac_param) { 'true' }
let(:params) do
{
cluster: {
name: 'new-cluster',
provider_gcp_attributes: {
gcp_project_id: 'gcp-project-12345',
legacy_abac: legacy_abac_param
}
}
}
end
def go
post :create_gcp, params.merge(group_id: group)
end
describe 'functionality' do
context 'when access token is valid' do
before do
stub_google_api_validate_token
end
it 'creates a new cluster' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Providers::Gcp.count }
cluster = group.clusters.first
expect(response).to redirect_to(group_cluster_path(group, cluster))
expect(cluster).to be_gcp
expect(cluster).to be_kubernetes
expect(cluster.provider_gcp).to be_legacy_abac
end
context 'when legacy_abac param is false' do
let(:legacy_abac_param) { 'false' }
it 'creates a new cluster with legacy_abac_disabled' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Providers::Gcp.count }
expect(group.clusters.first.provider_gcp).not_to be_legacy_abac
end
end
end
context 'when access token is expired' do
before do
stub_google_api_expired_token
end
it { expect(@valid_gcp_token).to be_falsey }
end
context 'when access token is not stored in session' do
it { expect(@valid_gcp_token).to be_falsey }
end
end
describe 'security' do
before do
allow_any_instance_of(described_class)
.to receive(:token_in_session).and_return('token')
allow_any_instance_of(described_class)
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create) do
OpenStruct.new(
self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
status: 'RUNNING'
)
end
allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
end
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
end
describe 'POST create for existing cluster' do
let(:params) do
{
cluster: {
name: 'new-cluster',
platform_kubernetes_attributes: {
api_url: 'http://my-url',
token: 'test'
}
}
}
end
def go
post :create_user, params.merge(group_id: group)
end
describe 'functionality' do
context 'when creates a cluster' do
it 'creates a new cluster' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Platforms::Kubernetes.count }
cluster = group.clusters.first
expect(response).to redirect_to(group_cluster_path(group, cluster))
expect(cluster).to be_user
expect(cluster).to be_kubernetes
end
end
context 'when creates a RBAC-enabled cluster' do
let(:params) do
{
cluster: {
name: 'new-cluster',
platform_kubernetes_attributes: {
api_url: 'http://my-url',
token: 'test',
authorization_type: 'rbac'
}
}
}
end
it 'creates a new cluster' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Platforms::Kubernetes.count }
cluster = group.clusters.first
expect(response).to redirect_to(group_cluster_path(group, cluster))
expect(cluster).to be_user
expect(cluster).to be_kubernetes
expect(cluster).to be_platform_kubernetes_rbac
end
end
end
describe 'security' do
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
end
describe 'GET cluster_status' do
let(:cluster) { create(:cluster, :providing_by_gcp, cluster_type: :group_type, groups: [group]) }
def go
get :cluster_status,
group_id: group.to_param,
id: cluster,
format: :json
end
describe 'functionality' do
it 'responds with matching schema' do
go
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('cluster_status')
end
it 'invokes schedule_status_update on each application' do
expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update)
go
end
end
describe 'security' do
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
end
describe 'GET show' do
let(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
def go
get :show,
group_id: group,
id: cluster
end
describe 'functionality' do
it 'renders view' do
go
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:cluster)).to eq(cluster)
end
end
describe 'security' do
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
end
describe 'PUT update' do
def go(format: :html)
put :update, params.merge(
group_id: group.to_param,
id: cluster,
format: format
)
end
let(:cluster) { create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group]) }
let(:params) do
{
cluster: {
enabled: false,
name: 'my-new-cluster-name'
}
}
end
it 'updates and redirects back to show page' do
go
cluster.reload
expect(response).to redirect_to(group_cluster_path(group, cluster))
expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name')
end
context 'when format is json' do
context 'when changing parameters' do
context 'when valid parameters are used' do
let(:params) do
{
cluster: {
enabled: false,
name: 'my-new-cluster-name'
}
}
end
it 'updates and redirects back to show page' do
go(format: :json)
cluster.reload
expect(response).to have_http_status(:no_content)
expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name')
end
end
context 'when invalid parameters are used' do
let(:params) do
{
cluster: {
enabled: false,
name: ''
}
}
end
it 'rejects changes' do
go(format: :json)
expect(response).to have_http_status(:bad_request)
end
end
end
end
describe 'security' do
set(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
end
describe 'DELETE destroy' do
let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
def go
delete :destroy,
group_id: group,
id: cluster
end
describe 'functionality' do
context 'when cluster is provided by GCP' do
context 'when cluster is created' do
it 'destroys and redirects back to clusters list' do
expect { go }
.to change { Clusters::Cluster.count }.by(-1)
.and change { Clusters::Platforms::Kubernetes.count }.by(-1)
.and change { Clusters::Providers::Gcp.count }.by(-1)
expect(response).to redirect_to(group_clusters_path(group))
expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
end
end
context 'when cluster is being created' do
let!(:cluster) { create(:cluster, :providing_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
it 'destroys and redirects back to clusters list' do
expect { go }
.to change { Clusters::Cluster.count }.by(-1)
.and change { Clusters::Providers::Gcp.count }.by(-1)
expect(response).to redirect_to(group_clusters_path(group))
expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
end
end
end
context 'when cluster is provided by user' do
let!(:cluster) { create(:cluster, :provided_by_user, :production_environment, cluster_type: :group_type, groups: [group]) }
it 'destroys and redirects back to clusters list' do
expect { go }
.to change { Clusters::Cluster.count }.by(-1)
.and change { Clusters::Platforms::Kubernetes.count }.by(-1)
.and change { Clusters::Providers::Gcp.count }.by(0)
expect(response).to redirect_to(group_clusters_path(group))
expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
end
end
end
describe 'security' do
set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
it { expect { go }.to be_denied_for(:external) }
end
end
context 'no group_id param' do
it 'does not respond to any action without group_id param' do
expect { get :index }.to raise_error(ActionController::UrlGenerationError)
end
end
end
...@@ -122,7 +122,7 @@ describe Projects::ClustersController do ...@@ -122,7 +122,7 @@ describe Projects::ClustersController do
it 'has new object' do it 'has new object' do
go go
expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::Cluster) expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
end end
end end
...@@ -143,7 +143,7 @@ describe Projects::ClustersController do ...@@ -143,7 +143,7 @@ describe Projects::ClustersController do
it 'has new object' do it 'has new object' do
go go
expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::Cluster) expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
end end
end end
...@@ -396,20 +396,6 @@ describe Projects::ClustersController do ...@@ -396,20 +396,6 @@ describe Projects::ClustersController do
end end
describe 'PUT update' do describe 'PUT update' do
let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let(:params) do
{
cluster: {
enabled: false,
name: 'my-new-cluster-name',
platform_kubernetes_attributes: {
namespace: 'my-namespace'
}
}
}
end
def go(format: :html) def go(format: :html)
put :update, params.merge(namespace_id: project.namespace.to_param, put :update, params.merge(namespace_id: project.namespace.to_param,
project_id: project.to_param, project_id: project.to_param,
...@@ -423,105 +409,73 @@ describe Projects::ClustersController do ...@@ -423,105 +409,73 @@ describe Projects::ClustersController do
stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace') stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
end end
context 'when cluster is provided by GCP' do let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
it "updates and redirects back to show page" do
go
cluster.reload
expect(response).to redirect_to(project_cluster_path(project, cluster))
expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
expect(cluster.enabled).to be_falsey
end
it "does not change cluster name" do
go
cluster.reload
expect(cluster.name).to eq('test-cluster')
end
context 'when cluster is being created' do
let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
it "rejects changes" do let(:params) do
go {
cluster: {
expect(response).to have_gitlab_http_status(:ok) enabled: false,
expect(response).to render_template(:show) name: 'my-new-cluster-name',
expect(cluster.enabled).to be_truthy platform_kubernetes_attributes: {
end namespace: 'my-namespace'
end
end
context 'when cluster is provided by user' do
let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
let(:params) do
{
cluster: {
enabled: false,
name: 'my-new-cluster-name',
platform_kubernetes_attributes: {
namespace: 'my-namespace'
}
} }
} }
end }
end
it "updates and redirects back to show page" do it "updates and redirects back to show page" do
go go
cluster.reload cluster.reload
expect(response).to redirect_to(project_cluster_path(project, cluster)) expect(response).to redirect_to(project_cluster_path(project, cluster))
expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.') expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
expect(cluster.enabled).to be_falsey expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name') expect(cluster.name).to eq('my-new-cluster-name')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace') expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
end end
context 'when format is json' do context 'when format is json' do
context 'when changing parameters' do context 'when changing parameters' do
context 'when valid parameters are used' do context 'when valid parameters are used' do
let(:params) do let(:params) do
{ {
cluster: { cluster: {
enabled: false, enabled: false,
name: 'my-new-cluster-name', name: 'my-new-cluster-name',
platform_kubernetes_attributes: { platform_kubernetes_attributes: {
namespace: 'my-namespace' namespace: 'my-namespace'
}
} }
} }
end }
end
it "updates and redirects back to show page" do it "updates and redirects back to show page" do
go(format: :json) go(format: :json)
cluster.reload cluster.reload
expect(response).to have_http_status(:no_content) expect(response).to have_http_status(:no_content)
expect(cluster.enabled).to be_falsey expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name') expect(cluster.name).to eq('my-new-cluster-name')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace') expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
end
end end
end
context 'when invalid parameters are used' do context 'when invalid parameters are used' do
let(:params) do let(:params) do
{ {
cluster: { cluster: {
enabled: false, enabled: false,
platform_kubernetes_attributes: { platform_kubernetes_attributes: {
namespace: 'my invalid namespace #@' namespace: 'my invalid namespace #@'
}
} }
} }
end }
end
it "rejects changes" do it "rejects changes" do
go(format: :json) go(format: :json)
expect(response).to have_http_status(:bad_request) expect(response).to have_http_status(:bad_request)
end
end end
end end
end end
......
...@@ -10,7 +10,7 @@ FactoryBot.define do ...@@ -10,7 +10,7 @@ FactoryBot.define do
username 'xxxxxx' username 'xxxxxx'
password 'xxxxxx' password 'xxxxxx'
after(:create) do |platform_kubernetes, evaluator| before(:create) do |platform_kubernetes, evaluator|
pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
platform_kubernetes.ca_cert = File.read(pem_file) platform_kubernetes.ca_cert = File.read(pem_file)
end end
......
...@@ -35,37 +35,6 @@ describe 'Clusters', :js do ...@@ -35,37 +35,6 @@ describe 'Clusters', :js do
expect(page).to have_selector('.gl-responsive-table-row', count: 2) expect(page).to have_selector('.gl-responsive-table-row', count: 2)
end end
context 'inline update of cluster' do
it 'user can update cluster' do
expect(page).to have_selector('.js-project-feature-toggle')
end
context 'with successful request' do
it 'user sees updated cluster' do
expect do
page.find('.js-project-feature-toggle').click
wait_for_requests
end.to change { cluster.reload.enabled }
expect(page).not_to have_selector('.is-checked')
expect(cluster.reload).not_to be_enabled
end
end
context 'with failed request' do
it 'user sees not update cluster and error message' do
expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false }
page.find('.js-project-feature-toggle').click
expect(page).to have_content('Something went wrong on our end.')
expect(page).to have_selector('.is-checked')
expect(cluster.reload).to be_enabled
end
end
end
context 'when user clicks on a cluster' do context 'when user clicks on a cluster' do
before do before do
click_link cluster.name click_link cluster.name
......
...@@ -343,4 +343,26 @@ describe Clusters::Cluster do ...@@ -343,4 +343,26 @@ describe Clusters::Cluster do
it { is_expected.to eq(false) } it { is_expected.to eq(false) }
end end
end end
describe '#allow_user_defined_namespace?' do
let(:cluster) { create(:cluster, :provided_by_gcp) }
subject { cluster.allow_user_defined_namespace? }
context 'project type cluster' do
it { is_expected.to be_truthy }
end
context 'group type cluster' do
let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
it { is_expected.to be_falsey }
end
context 'instance type cluster' do
let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
it { is_expected.to be_falsey }
end
end
end end
...@@ -58,6 +58,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -58,6 +58,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end end
context 'for group cluster' do
let(:namespace) { 'namespace-123' }
let(:cluster) { build(:cluster, :group, :provided_by_user) }
let(:kubernetes) { cluster.platform_kubernetes }
before do
kubernetes.namespace = namespace
end
it { is_expected.to be_falsey }
end
end end
context 'when validates api_url' do context 'when validates api_url' do
......
...@@ -24,5 +24,47 @@ describe Clusters::ClusterPolicy, :models do ...@@ -24,5 +24,47 @@ describe Clusters::ClusterPolicy, :models do
it { expect(policy).to be_allowed :update_cluster } it { expect(policy).to be_allowed :update_cluster }
it { expect(policy).to be_allowed :admin_cluster } it { expect(policy).to be_allowed :admin_cluster }
end end
context 'group cluster' do
let(:cluster) { create(:cluster, :group) }
let(:group) { cluster.group }
let(:project) { create(:project, namespace: group) }
context 'when group developer' do
before do
group.add_developer(user)
end
it { expect(policy).to be_disallowed :update_cluster }
it { expect(policy).to be_disallowed :admin_cluster }
end
context 'when group maintainer' do
before do
group.add_maintainer(user)
end
it { expect(policy).to be_allowed :update_cluster }
it { expect(policy).to be_allowed :admin_cluster }
end
context 'when project maintainer' do
before do
project.add_maintainer(user)
end
it { expect(policy).to be_disallowed :update_cluster }
it { expect(policy).to be_disallowed :admin_cluster }
end
context 'when project developer' do
before do
project.add_developer(user)
end
it { expect(policy).to be_disallowed :update_cluster }
it { expect(policy).to be_disallowed :admin_cluster }
end
end
end end
end end
...@@ -21,7 +21,11 @@ describe GroupPolicy do ...@@ -21,7 +21,11 @@ describe GroupPolicy do
let(:maintainer_permissions) do let(:maintainer_permissions) do
[ [
:create_projects :create_projects,
:read_cluster,
:create_cluster,
:update_cluster,
:admin_cluster
] ]
end end
......
...@@ -163,7 +163,7 @@ describe ProjectPolicy do ...@@ -163,7 +163,7 @@ describe ProjectPolicy do
:create_build, :read_build, :update_build, :admin_build, :destroy_build, :create_build, :read_build, :update_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule, :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment, :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
:create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster, :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
] ]
...@@ -182,7 +182,7 @@ describe ProjectPolicy do ...@@ -182,7 +182,7 @@ describe ProjectPolicy do
:create_build, :read_build, :update_build, :admin_build, :destroy_build, :create_build, :read_build, :update_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule, :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment, :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
:create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster, :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
] ]
......
...@@ -82,5 +82,12 @@ describe Clusters::ClusterPresenter do ...@@ -82,5 +82,12 @@ describe Clusters::ClusterPresenter do
it { is_expected.to eq(project_cluster_path(project, cluster)) } it { is_expected.to eq(project_cluster_path(project, cluster)) }
end end
context 'group_type cluster' do
let(:group) { cluster.group }
let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
it { is_expected.to eq(group_cluster_path(group, cluster)) }
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe GroupClusterablePresenter do
include Gitlab::Routing.url_helpers
let(:presenter) { described_class.new(group) }
let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
let(:group) { cluster.group }
describe '#can_create_cluster?' do
let(:user) { create(:user) }
subject { presenter.can_create_cluster? }
before do
allow(presenter).to receive(:current_user).and_return(user)
end
context 'when user can create' do
before do
group.add_maintainer(user)
end
it { is_expected.to be_truthy }
end
context 'when user cannot create' do
it { is_expected.to be_falsey }
end
end
describe '#index_path' do
subject { presenter.index_path }
it { is_expected.to eq(group_clusters_path(group)) }
end
describe '#new_path' do
subject { presenter.new_path }
it { is_expected.to eq(new_group_cluster_path(group)) }
end
describe '#create_user_clusters_path' do
subject { presenter.create_user_clusters_path }
it { is_expected.to eq(create_user_group_clusters_path(group)) }
end
describe '#create_gcp_clusters_path' do
subject { presenter.create_gcp_clusters_path }
it { is_expected.to eq(create_gcp_group_clusters_path(group)) }
end
describe '#cluster_status_cluster_path' do
subject { presenter.cluster_status_cluster_path(cluster) }
it { is_expected.to eq(cluster_status_group_cluster_path(group, cluster)) }
end
describe '#install_applications_cluster_path' do
let(:application) { :helm }
subject { presenter.install_applications_cluster_path(cluster, application) }
it { is_expected.to eq(install_applications_group_cluster_path(group, cluster, application)) }
end
describe '#cluster_path' do
subject { presenter.cluster_path(cluster) }
it { is_expected.to eq(group_cluster_path(group, cluster)) }
end
end
...@@ -60,14 +60,6 @@ describe Clusters::Applications::CreateService do ...@@ -60,14 +60,6 @@ describe Clusters::Applications::CreateService do
end end
end end
context 'invalid application' do
let(:params) { { application: 'non-existent' } }
it 'raises an error' do
expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
end
end
context 'knative application' do context 'knative application' do
let(:params) do let(:params) do
{ {
...@@ -100,5 +92,39 @@ describe Clusters::Applications::CreateService do ...@@ -100,5 +92,39 @@ describe Clusters::Applications::CreateService do
expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError) expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
end end
end end
context 'group cluster' do
let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
using RSpec::Parameterized::TableSyntax
before do
allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
end
where(:application, :association, :allowed) do
'helm' | :application_helm | true
'ingress' | :application_ingress | true
'runner' | :application_runner | false
'jupyter' | :application_jupyter | false
'prometheus' | :application_prometheus | false
end
with_them do
let(:params) { { application: application } }
it 'executes for each application' do
if allowed
expect do
subject
cluster.reload
end.to change(cluster, association)
else
expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
end
end
end
end
end end
end end
...@@ -62,5 +62,32 @@ describe Clusters::UpdateService do ...@@ -62,5 +62,32 @@ describe Clusters::UpdateService do
expect(cluster.errors[:"platform_kubernetes.namespace"]).to be_present expect(cluster.errors[:"platform_kubernetes.namespace"]).to be_present
end end
end end
context 'when cluster is provided by GCP' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:params) do
{
name: 'my-new-name'
}
end
it 'does not change cluster name' do
is_expected.to eq(false)
cluster.reload
expect(cluster.name).to eq('test-cluster')
end
context 'when cluster is being created' do
let(:cluster) { create(:cluster, :providing_by_gcp) }
it 'rejects changes' do
is_expected.to eq(false)
expect(cluster.errors.full_messages).to include('cannot modify during creation')
end
end
end
end end
end end
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