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';
import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue';
import Applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
/**
......@@ -31,6 +31,7 @@ export default class Clusters {
installKnativePath,
installPrometheusPath,
managePrometheusPath,
clusterType,
clusterStatus,
clusterStatusReason,
helpPath,
......@@ -67,7 +68,7 @@ export default class Clusters {
initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications();
this.initApplications(clusterType);
if (this.store.state.status !== 'created') {
this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
......@@ -79,23 +80,21 @@ export default class Clusters {
}
}
initApplications() {
initApplications(type) {
const { store } = this;
const el = document.querySelector('#js-cluster-applications');
this.applications = new Vue({
el,
components: {
applications,
},
data() {
return {
state: store.state,
};
},
render(createElement) {
return createElement('applications', {
return createElement(Applications, {
props: {
type,
applications: this.state.applications,
helpPath: this.state.helpPath,
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';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.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 {
components: {
......@@ -21,6 +21,11 @@ export default {
clipboardButton,
},
props: {
type: {
type: String,
required: false,
default: CLUSTER_TYPE.PROJECT,
},
applications: {
type: Object,
required: false,
......@@ -59,6 +64,9 @@ export default {
prometheusLogo,
}),
computed: {
isProjectCluster() {
return this.type === CLUSTER_TYPE.PROJECT;
},
helmInstalled() {
return (
this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
......@@ -281,6 +289,7 @@ export default {
</div>
</application-row>
<application-row
v-if="isProjectCluster"
id="prometheus"
:logo-url="prometheusLogo"
:title="applications.prometheus.title"
......@@ -299,6 +308,7 @@ export default {
</div>
</application-row>
<application-row
v-if="isProjectCluster"
id="runner"
:logo-url="gitlabLogo"
:title="applications.runner.title"
......@@ -317,6 +327,7 @@ export default {
</div>
</application-row>
<application-row
v-if="isProjectCluster"
id="jupyter"
:logo-url="jupyterhubLogo"
: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
export const APPLICATION_STATUS = {
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', () => {
new ClustersIndex(); // eslint-disable-line no-new
initDismissableCallout('.gcp-signup-offer');
});
......@@ -25,6 +25,12 @@
.cluster-application-row {
border-bottom: 1px solid $border-color;
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 @@
padding: $gl-padding-top $gl-padding;
}
.card {
margin-bottom: $gl-vert-padding;
}
.empty-state .svg-content img {
width: 145px;
}
......@@ -80,6 +90,31 @@
.top-area .nav-controls > .btn.btn-add-cluster {
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 {
......
......@@ -183,13 +183,13 @@ class Clusters::ClustersController < Clusters::BaseController
def gcp_cluster
@gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
end
end.present(current_user: current_user)
end
def user_cluster
@user_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_platform_kubernetes
end
end.present(current_user: current_user)
end
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
can?(current_user, "read_group_#{resource}".to_sym, @group)
end
if can?(current_user, :read_cluster, @group) && Feature.enabled?(:group_clusters)
links << :kubernetes
end
if can?(current_user, :admin_group, @group)
links << :settings
end
......
......@@ -29,7 +29,7 @@ module Clusters
# we force autosave to happen when we save `Cluster` model
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_ingress, class_name: 'Clusters::Applications::Ingress'
......@@ -144,6 +144,10 @@ module Clusters
)
end
def allow_user_defined_namespace?
project_type?
end
private
def restrict_modification
......
......@@ -38,6 +38,8 @@ module Clusters
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)
validates :api_url, url: true, presence: true
validates :token, presence: true
......@@ -52,6 +54,7 @@ module Clusters
delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, 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
alias_method :active?, :enabled?
......@@ -150,7 +153,8 @@ module Clusters
end
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
raise "Either username/password or token is required to access API"
......@@ -207,6 +211,12 @@ module Clusters
self.token = self.token&.strip
end
def no_namespace
if namespace
errors.add(:namespace, 'only allowed for project cluster')
end
end
def prevent_modification
return unless managed?
......
......@@ -4,11 +4,7 @@ module Clusters
class ClusterPolicy < BasePolicy
alias_method :cluster, :subject
delegate { cluster.group }
delegate { cluster.project }
rule { can?(:maintainer_access) }.policy do
enable :update_cluster
enable :admin_cluster
end
end
end
......@@ -65,6 +65,10 @@ class GroupPolicy < BasePolicy
enable :create_projects
enable :admin_pipeline
enable :admin_build
enable :read_cluster
enable :create_cluster
enable :update_cluster
enable :admin_cluster
end
rule { owner }.policy do
......
......@@ -258,6 +258,8 @@ class ProjectPolicy < BasePolicy
enable :update_pages
enable :read_cluster
enable :create_cluster
enable :update_cluster
enable :admin_cluster
enable :create_environment_terminal
end
......
......@@ -43,4 +43,16 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
def cluster_path(cluster, params = {})
raise NotImplementedError
end
def empty_state_help_text
nil
end
def sidebar_text
raise NotImplementedError
end
def learn_more_link
raise NotImplementedError
end
end
......@@ -15,6 +15,8 @@ module Clusters
def show_path
if cluster.project_type?
project_cluster_path(project, cluster)
elsif cluster.group_type?
group_cluster_path(group, cluster)
else
raise NotImplementedError
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
class ProjectClusterablePresenter < ClusterablePresenter
extend ::Gitlab::Utils::Override
include ActionView::Helpers::UrlHelper
override :cluster_status_cluster_path
def cluster_status_cluster_path(cluster, params = {})
cluster_status_project_cluster_path(clusterable, cluster, params)
end
override :install_applications_cluster_path
def install_applications_cluster_path(cluster, application)
install_applications_project_cluster_path(clusterable, cluster, application)
end
override :cluster_path
def cluster_path(cluster, params = {})
project_cluster_path(clusterable, cluster, params)
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
......@@ -42,7 +42,16 @@ module Clusters
def builders
{
"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 },
"runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner },
"jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter },
......
......@@ -36,6 +36,10 @@ module Clusters
case clusterable
when ::Project
{ cluster_type: :project_type, projects: [clusterable] }
when ::Group
{ cluster_type: :group_type, groups: [clusterable] }
else
raise NotImplementedError
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
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
.table-mobile-content
= link_to cluster.name, cluster.show_path
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
.table-mobile-content= cluster.environment_scope
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
.table-mobile-content= cluster.platform_kubernetes&.actual_namespace
.table-section.section-10
.table-mobile-header{ role: "rowheader" }
.table-mobile-content
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_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')
.card
.card-body.gl-responsive-table-row
.table-section.section-60
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
.table-mobile-content
= link_to cluster.name, cluster.show_path
- unless cluster.enabled?
%span.badge.badge-danger Connection disabled
.table-section.section-25
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
.table-mobile-content= cluster.environment_scope
.table-section.section-15.text-right
.table-mobile-header{ role: "rowheader" }
.table-mobile-content
%span.badge.badge-light
= cluster.project_type? ? s_("ClusterIntegration|Project cluster") : s_("ClusterIntegration|Group cluster")
......@@ -4,8 +4,10 @@
.col-12
.text-content
%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= 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}
%p
= 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?
.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
= s_('ClusterIntegration|Kubernetes cluster integration')
= s_('ClusterIntegration|Add a Kubernetes cluster integration')
%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
= 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 @@
= s_('ClusterIntegration|Show')
= clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
.form-group
= 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')
- if @cluster.allow_user_defined_namespace?
.form-group
= 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-check
......
......@@ -10,14 +10,13 @@
.top-area.adjust
.nav-text
= 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" }
.table-section.section-30{ role: "rowheader" }
.table-section.section-60{ role: "rowheader" }
= s_("ClusterIntegration|Kubernetes cluster")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Environment scope")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Project namespace")
.table-section.section-10{ role: "rowheader" }
- @clusters.each do |cluster|
= render "cluster", cluster: cluster.present(current_user: current_user)
......
......@@ -15,6 +15,7 @@
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_type: @cluster.cluster_type,
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
......
......@@ -21,9 +21,10 @@
= 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'
.form-group
= 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')
- if @user_cluster.allow_user_defined_namespace?
.form-group
= 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-check
......
......@@ -22,9 +22,10 @@
%button.js-show-cluster-token.btn-blank{ type: 'button' }
= s_('ClusterIntegration|Show')
.form-group
= 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')
- if @cluster.allow_user_defined_namespace?
.form-group
= 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-check
......
......@@ -116,6 +116,19 @@
%strong.fly-out-top-item-name
= _('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)
= nav_link(path: group_nav_link_paths) 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
resource :avatar, only: [:destroy]
concerns :clusterable
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
......
......@@ -1364,6 +1364,15 @@ msgstr ""
msgid "ClusterIntegration|Add Kubernetes cluster"
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"
msgstr ""
......@@ -1466,6 +1475,9 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Group cluster"
msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
......@@ -1520,9 +1532,6 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster integration"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
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"
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 ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
......@@ -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}."
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 ""
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 ""
msgid "ClusterIntegration|Machine type"
......@@ -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."
msgstr ""
msgid "ClusterIntegration|Project cluster"
msgstr ""
msgid "ClusterIntegration|Project namespace"
msgstr ""
......@@ -1679,9 +1694,6 @@ msgstr ""
msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters."
msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes Cluster"
msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes cluster"
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
This diff is collapsed.
......@@ -122,7 +122,7 @@ describe Projects::ClustersController do
it 'has new object' do
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
......@@ -143,7 +143,7 @@ describe Projects::ClustersController do
it 'has new object' do
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
......@@ -396,20 +396,6 @@ describe Projects::ClustersController do
end
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)
put :update, params.merge(namespace_id: project.namespace.to_param,
project_id: project.to_param,
......@@ -423,105 +409,73 @@ describe Projects::ClustersController do
stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
end
context 'when cluster is provided by GCP' do
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]) }
let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
it "rejects changes" do
go
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
expect(cluster.enabled).to be_truthy
end
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'
}
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
go
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
expect(cluster.name).to eq('my-new-cluster-name')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
end
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
expect(cluster.name).to eq('my-new-cluster-name')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
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',
platform_kubernetes_attributes: {
namespace: 'my-namespace'
}
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',
platform_kubernetes_attributes: {
namespace: 'my-namespace'
}
}
end
}
end
it "updates and redirects back to show page" do
go(format: :json)
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')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
end
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')
expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
end
end
context 'when invalid parameters are used' do
let(:params) do
{
cluster: {
enabled: false,
platform_kubernetes_attributes: {
namespace: 'my invalid namespace #@'
}
context 'when invalid parameters are used' do
let(:params) do
{
cluster: {
enabled: false,
platform_kubernetes_attributes: {
namespace: 'my invalid namespace #@'
}
}
end
}
end
it "rejects changes" do
go(format: :json)
it "rejects changes" do
go(format: :json)
expect(response).to have_http_status(:bad_request)
end
expect(response).to have_http_status(:bad_request)
end
end
end
......
......@@ -10,7 +10,7 @@ FactoryBot.define do
username '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'))
platform_kubernetes.ca_cert = File.read(pem_file)
end
......
......@@ -35,37 +35,6 @@ describe 'Clusters', :js do
expect(page).to have_selector('.gl-responsive-table-row', count: 2)
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
before do
click_link cluster.name
......
......@@ -343,4 +343,26 @@ describe Clusters::Cluster do
it { is_expected.to eq(false) }
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
......@@ -58,6 +58,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_truthy }
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
context 'when validates api_url' do
......
......@@ -24,5 +24,47 @@ describe Clusters::ClusterPolicy, :models do
it { expect(policy).to be_allowed :update_cluster }
it { expect(policy).to be_allowed :admin_cluster }
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
......@@ -21,7 +21,11 @@ describe GroupPolicy do
let(:maintainer_permissions) do
[
:create_projects
:create_projects,
:read_cluster,
:create_cluster,
:update_cluster,
:admin_cluster
]
end
......
......@@ -163,7 +163,7 @@ describe ProjectPolicy do
: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_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
]
......@@ -182,7 +182,7 @@ describe ProjectPolicy do
: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_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
]
......
......@@ -82,5 +82,12 @@ describe Clusters::ClusterPresenter do
it { is_expected.to eq(project_cluster_path(project, cluster)) }
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
# 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
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
let(:params) do
{
......@@ -100,5 +92,39 @@ describe Clusters::Applications::CreateService do
expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
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
......@@ -62,5 +62,32 @@ describe Clusters::UpdateService do
expect(cluster.errors[:"platform_kubernetes.namespace"]).to be_present
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
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