Commit 46fd3159 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'top_level_clusters_controller' into 'master'

Top level clusters controller

See merge request gitlab-org/gitlab-ce!22438
parents 473262a0 fec21f55
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
document.addEventListener('DOMContentLoaded', () => {
initGkeDropdowns();
});
# frozen_string_literal: true
class Clusters::ApplicationsController < Clusters::BaseController
before_action :cluster
before_action :authorize_create_cluster!, only: [:create]
def create
Clusters::Applications::CreateService
.new(@cluster, current_user, create_cluster_application_params)
.execute(request)
head :no_content
rescue Clusters::Applications::CreateService::InvalidApplicationError
render_404
rescue StandardError
head :bad_request
end
private
def cluster
@cluster ||= clusterable.clusters.find(params[:id]) || render_404
end
def create_cluster_application_params
params.permit(:application, :hostname)
end
end
# frozen_string_literal: true
class Clusters::BaseController < ApplicationController
include RoutableActions
skip_before_action :authenticate_user!
before_action :authorize_read_cluster!
helper_method :clusterable
private
def cluster
@cluster ||= clusterable.clusters.find(params[:id])
.present(current_user: current_user)
end
def authorize_update_cluster!
access_denied! unless can?(current_user, :update_cluster, cluster)
end
def authorize_admin_cluster!
access_denied! unless can?(current_user, :admin_cluster, cluster)
end
def authorize_read_cluster!
access_denied! unless can?(current_user, :read_cluster, clusterable)
end
def authorize_create_cluster!
access_denied! unless can?(current_user, :create_cluster, clusterable)
end
def clusterable
raise NotImplementedError
end
end
# frozen_string_literal: true
class Clusters::ClustersController < Clusters::BaseController
include RoutableActions
before_action :cluster, except: [:index, :new, :create_gcp, :create_user]
before_action :generate_gcp_authorize_url, only: [:new]
before_action :validate_gcp_token, only: [:new]
before_action :gcp_cluster, only: [:new]
before_action :user_cluster, only: [:new]
before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:cluster_status]
helper_method :token_in_session
STATUS_POLLING_INTERVAL = 10_000
def index
clusters = ClustersFinder.new(clusterable, current_user, :all).execute
@clusters = clusters.page(params[:page]).per(20)
end
def new
end
# Overridding ActionController::Metal#status is NOT a good idea
def cluster_status
respond_to do |format|
format.json do
Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL)
render json: ClusterSerializer
.new(current_user: @current_user)
.represent_status(@cluster)
end
end
end
def show
end
def update
Clusters::UpdateService
.new(current_user, update_params)
.execute(cluster)
if cluster.valid?
respond_to do |format|
format.json do
head :no_content
end
format.html do
flash[:notice] = _('Kubernetes cluster was successfully updated.')
redirect_to cluster.show_path
end
end
else
respond_to do |format|
format.json { head :bad_request }
format.html { render :show }
end
end
end
def destroy
if cluster.destroy
flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
redirect_to clusterable.index_path, status: :found
else
flash[:notice] = _('Kubernetes cluster integration was not removed.')
render :show
end
end
def create_gcp
@gcp_cluster = ::Clusters::CreateService
.new(current_user, create_gcp_cluster_params)
.execute(access_token: token_in_session)
.present(current_user: current_user)
if @gcp_cluster.persisted?
redirect_to @gcp_cluster.show_path
else
generate_gcp_authorize_url
validate_gcp_token
user_cluster
render :new, locals: { active_tab: 'gcp' }
end
end
def create_user
@user_cluster = ::Clusters::CreateService
.new(current_user, create_user_cluster_params)
.execute(access_token: token_in_session)
.present(current_user: current_user)
if @user_cluster.persisted?
redirect_to @user_cluster.show_path
else
generate_gcp_authorize_url
validate_gcp_token
gcp_cluster
render :new, locals: { active_tab: 'user' }
end
end
private
def update_params
if cluster.managed?
params.require(:cluster).permit(
:enabled,
:environment_scope,
platform_kubernetes_attributes: [
:namespace
]
)
else
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
platform_kubernetes_attributes: [
:api_url,
:token,
:ca_cert,
:namespace
]
)
end
end
def create_gcp_cluster_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type,
:legacy_abac
]).merge(
provider_type: :gcp,
platform_type: :kubernetes,
clusterable: clusterable.subject
)
end
def create_user_cluster_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
platform_kubernetes_attributes: [
:namespace,
:api_url,
:token,
:ca_cert,
:authorization_type
]).merge(
provider_type: :user,
platform_type: :kubernetes,
clusterable: clusterable.subject
)
end
def generate_gcp_authorize_url
state = generate_session_key_redirect(clusterable.new_path.to_s)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,
state: state).authorize_url
rescue GoogleApi::Auth::ConfigMissingError
# no-op
end
def gcp_cluster
@gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
end
end
def user_cluster
@user_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_platform_kubernetes
end
end
def validate_gcp_token
@valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
.validate_token(expires_at_in_session)
end
def token_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
def expires_at_in_session
@expires_at_in_session ||=
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end
def generate_session_key_redirect(uri)
GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
session[key] = uri
end
end
def update_applications_status
@cluster.applications.each(&:schedule_status_update)
end
end
# frozen_string_literal: true
module ProjectUnauthorized
extend ActiveSupport::Concern
# EE would override this
def project_unauthorized_proc
# no-op
end
end
...@@ -3,23 +3,25 @@ ...@@ -3,23 +3,25 @@
module RoutableActions module RoutableActions
extend ActiveSupport::Concern extend ActiveSupport::Concern
def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil) def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil, not_found_or_authorized_proc: nil)
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?) routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
if routable_authorized?(routable, extra_authorization_proc) if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path) ensure_canonical_path(routable, requested_full_path)
routable routable
else else
handle_not_found_or_authorized(routable) if not_found_or_authorized_proc
not_found_or_authorized_proc.call(routable)
end
route_not_found unless performed?
nil nil
end end
end end
# This is overridden in gitlab-ee.
def handle_not_found_or_authorized(_routable)
route_not_found
end
def routable_authorized?(routable, extra_authorization_proc) def routable_authorized?(routable, extra_authorization_proc)
return false unless routable
action = :"read_#{routable.class.to_s.underscore}" action = :"read_#{routable.class.to_s.underscore}"
return false unless can?(current_user, action, routable) return false unless can?(current_user, action, routable)
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
class Projects::ApplicationController < ApplicationController class Projects::ApplicationController < ApplicationController
include CookiesHelper include CookiesHelper
include RoutableActions include RoutableActions
include ProjectUnauthorized
include ChecksCollaboration include ChecksCollaboration
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
...@@ -21,7 +22,7 @@ class Projects::ApplicationController < ApplicationController ...@@ -21,7 +22,7 @@ class Projects::ApplicationController < ApplicationController
path = File.join(params[:namespace_id], params[:project_id] || params[:id]) path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? } auth_proc = ->(project) { !project.pending_delete? }
@project = find_routable!(Project, path, extra_authorization_proc: auth_proc) @project = find_routable!(Project, path, extra_authorization_proc: auth_proc, not_found_or_authorized_proc: project_unauthorized_proc)
end end
def build_canonical_path(project) def build_canonical_path(project)
......
# frozen_string_literal: true # frozen_string_literal: true
class Projects::Clusters::ApplicationsController < Projects::ApplicationController class Projects::Clusters::ApplicationsController < Clusters::ApplicationsController
before_action :cluster include ProjectUnauthorized
before_action :authorize_read_cluster!
before_action :authorize_create_cluster!, only: [:create]
def create prepend_before_action :project
Clusters::Applications::CreateService
.new(@cluster, current_user, create_cluster_application_params)
.execute(request)
head :no_content
rescue Clusters::Applications::CreateService::InvalidApplicationError
render_404
rescue StandardError
head :bad_request
end
private private
def cluster def clusterable
@cluster ||= project.clusters.find(params[:id]) || render_404 @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user)
end end
def create_cluster_application_params def project
params.permit(:application, :hostname) @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
class Projects::ClustersController < Projects::ApplicationController class Projects::ClustersController < Clusters::ClustersController
before_action :cluster, except: [:index, :new, :create_gcp, :create_user] include ProjectUnauthorized
before_action :authorize_read_cluster!
before_action :generate_gcp_authorize_url, only: [:new]
before_action :validate_gcp_token, only: [:new]
before_action :gcp_cluster, only: [:new]
before_action :user_cluster, only: [:new]
before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:status]
helper_method :token_in_session
STATUS_POLLING_INTERVAL = 10_000 prepend_before_action :project
before_action :repository
def index layout 'project'
clusters = ClustersFinder.new(project, current_user, :all).execute
@clusters = clusters.page(params[:page]).per(20)
end
def new
end
def status
respond_to do |format|
format.json do
Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL)
render json: ClusterSerializer
.new(project: @project, current_user: @current_user)
.represent_status(@cluster)
end
end
end
def show
end
def update
Clusters::UpdateService
.new(current_user, update_params)
.execute(cluster)
if cluster.valid?
respond_to do |format|
format.json do
head :no_content
end
format.html do
flash[:notice] = _('Kubernetes cluster was successfully updated.')
redirect_to project_cluster_path(project, cluster)
end
end
else
respond_to do |format|
format.json { head :bad_request }
format.html { render :show }
end
end
end
def destroy
if cluster.destroy
flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
redirect_to project_clusters_path(project), status: :found
else
flash[:notice] = _('Kubernetes cluster integration was not removed.')
render :show
end
end
def create_gcp
@gcp_cluster = ::Clusters::CreateService
.new(current_user, create_gcp_cluster_params)
.execute(project: project, access_token: token_in_session)
if @gcp_cluster.persisted?
redirect_to project_cluster_path(project, @gcp_cluster)
else
generate_gcp_authorize_url
validate_gcp_token
user_cluster
render :new, locals: { active_tab: 'gcp' }
end
end
def create_user
@user_cluster = ::Clusters::CreateService
.new(current_user, create_user_cluster_params)
.execute(project: project, access_token: token_in_session)
if @user_cluster.persisted?
redirect_to project_cluster_path(project, @user_cluster)
else
generate_gcp_authorize_url
validate_gcp_token
gcp_cluster
render :new, locals: { active_tab: 'user' }
end
end
private private
def cluster def clusterable
@cluster ||= project.clusters.find(params[:id]) @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user)
.present(current_user: current_user)
end
def update_params
if cluster.managed?
params.require(:cluster).permit(
:enabled,
:environment_scope,
platform_kubernetes_attributes: [
:namespace
]
)
else
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
platform_kubernetes_attributes: [
:api_url,
:token,
:ca_cert,
:namespace
]
)
end
end
def create_gcp_cluster_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type,
:legacy_abac
]).merge(
provider_type: :gcp,
platform_type: :kubernetes
)
end
def create_user_cluster_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
platform_kubernetes_attributes: [
:namespace,
:api_url,
:token,
:ca_cert,
:authorization_type
]).merge(
provider_type: :user,
platform_type: :kubernetes
)
end
def generate_gcp_authorize_url
state = generate_session_key_redirect(new_project_cluster_path(@project).to_s)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,
state: state).authorize_url
rescue GoogleApi::Auth::ConfigMissingError
# no-op
end
def gcp_cluster
@gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
end
end
def user_cluster
@user_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_platform_kubernetes
end
end
def validate_gcp_token
@valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
.validate_token(expires_at_in_session)
end
def token_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
def expires_at_in_session
@expires_at_in_session ||=
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end
def generate_session_key_redirect(uri)
GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
session[key] = uri
end
end
def authorize_update_cluster!
access_denied! unless can?(current_user, :update_cluster, cluster)
end end
def authorize_admin_cluster! def project
access_denied! unless can?(current_user, :admin_cluster, cluster) @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
end end
def update_applications_status def repository
@cluster.applications.each(&:schedule_status_update) @repository ||= project.repository
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
class ClustersFinder class ClustersFinder
def initialize(project, user, scope) def initialize(clusterable, user, scope)
@project = project @clusterable = clusterable
@user = user @user = user
@scope = scope || :active @scope = scope || :active
end end
def execute def execute
clusters = project.clusters clusters = clusterable.clusters
filter_by_scope(clusters) filter_by_scope(clusters)
end end
private private
attr_reader :project, :user, :scope attr_reader :clusterable, :user, :scope
def filter_by_scope(clusters) def filter_by_scope(clusters)
case scope.to_sym case scope.to_sym
......
# frozen_string_literal: true # frozen_string_literal: true
module ClustersHelper module ClustersHelper
def has_multiple_clusters?(project) # EE overrides this
def has_multiple_clusters?
false false
end end
...@@ -10,7 +11,7 @@ module ClustersHelper ...@@ -10,7 +11,7 @@ module ClustersHelper
return unless show_gcp_signup_offer? return unless show_gcp_signup_offer?
content_tag :section, class: 'no-animate expanded' do content_tag :section, class: 'no-animate expanded' do
render 'projects/clusters/gcp_signup_offer_banner' render 'clusters/clusters/gcp_signup_offer_banner'
end end
end end
end end
# frozen_string_literal: true
class ClusterablePresenter < Gitlab::View::Presenter::Delegated
presents :clusterable
def self.fabricate(clusterable, **attributes)
presenter_class = "#{clusterable.class.name}ClusterablePresenter".constantize
attributes_with_presenter_class = attributes.merge(presenter_class: presenter_class)
Gitlab::View::Presenter::Factory
.new(clusterable, attributes_with_presenter_class)
.fabricate!
end
def can_create_cluster?
can?(current_user, :create_cluster, clusterable)
end
def index_path
polymorphic_path([clusterable, :clusters])
end
def new_path
new_polymorphic_path([clusterable, :cluster])
end
def create_user_clusters_path
polymorphic_path([clusterable, :clusters], action: :create_user)
end
def create_gcp_clusters_path
polymorphic_path([clusterable, :clusters], action: :create_gcp)
end
def cluster_status_cluster_path(cluster, params = {})
raise NotImplementedError
end
def install_applications_cluster_path(cluster, application)
raise NotImplementedError
end
def cluster_path(cluster, params = {})
raise NotImplementedError
end
end
...@@ -11,5 +11,13 @@ module Clusters ...@@ -11,5 +11,13 @@ module Clusters
def can_toggle_cluster? def can_toggle_cluster?
can?(current_user, :update_cluster, cluster) && created? can?(current_user, :update_cluster, cluster) && created?
end end
def show_path
if cluster.project_type?
project_cluster_path(project, cluster)
else
raise NotImplementedError
end
end
end end
end end
# frozen_string_literal: true
class ProjectClusterablePresenter < ClusterablePresenter
def cluster_status_cluster_path(cluster, params = {})
cluster_status_project_cluster_path(clusterable, cluster, params)
end
def install_applications_cluster_path(cluster, application)
install_applications_project_cluster_path(clusterable, cluster, application)
end
def cluster_path(cluster, params = {})
project_cluster_path(clusterable, cluster, params)
end
end
...@@ -8,10 +8,11 @@ module Clusters ...@@ -8,10 +8,11 @@ module Clusters
@current_user, @params = user, params.dup @current_user, @params = user, params.dup
end end
def execute(project:, access_token: nil) def execute(access_token: nil)
raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?(project) raise ArgumentError, 'Unknown clusterable provided' unless clusterable
raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?
cluster_params = params.merge(user: current_user, cluster_type: :project_type, projects: [project]) cluster_params = params.merge(user: current_user).merge(clusterable_params)
cluster_params[:provider_gcp_attributes].try do |provider| cluster_params[:provider_gcp_attributes].try do |provider|
provider[:access_token] = access_token provider[:access_token] = access_token
end end
...@@ -27,9 +28,20 @@ module Clusters ...@@ -27,9 +28,20 @@ module Clusters
Clusters::Cluster.create(cluster_params) Clusters::Cluster.create(cluster_params)
end end
def clusterable
@clusterable ||= params.delete(:clusterable)
end
def clusterable_params
case clusterable
when ::Project
{ cluster_type: :project_type, projects: [clusterable] }
end
end
# EE would override this method # EE would override this method
def can_create_cluster?(project) def can_create_cluster?
project.clusters.empty? clusterable.clusters.empty?
end end
end end
end end
...@@ -12,4 +12,4 @@ ...@@ -12,4 +12,4 @@
= s_('ClusterIntegration|Remove Kubernetes cluster integration') = s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p %p
= s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.") = s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.")
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")}) = link_to(s_('ClusterIntegration|Remove integration'), clusterable.cluster_path(@cluster), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")})
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.table-section.section-30 .table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster") .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
.table-mobile-content .table-mobile-content
= link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster) = link_to cluster.name, cluster.show_path
.table-section.section-30 .table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope") .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
.table-mobile-content= cluster.environment_scope .table-mobile-content= cluster.environment_scope
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"), "aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"),
disabled: !cluster.can_toggle_cluster?, disabled: !cluster.can_toggle_cluster?,
data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } data: { endpoint: clusterable.cluster_path(cluster, format: :json) } }
%input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? } %input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? }
= icon("spinner spin", class: "loading-icon") = icon("spinner spin", class: "loading-icon")
%span.toggle-icon %span.toggle-icon
......
...@@ -7,6 +7,6 @@ ...@@ -7,6 +7,6 @@
- link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') - 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. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
- if can?(current_user, :create_cluster, @project) - if clusterable.can_create_cluster?
.text-center .text-center
= link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success' = link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success'
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
= form_errors(@cluster) = form_errors(@cluster)
.form-group .form-group
%h5= s_('ClusterIntegration|Integration status') %h5= s_('ClusterIntegration|Integration status')
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
.form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.') .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.')
- if has_multiple_clusters?(@project) - if has_multiple_clusters?
.form-group .form-group
%h5= s_('ClusterIntegration|Environment scope') %h5= s_('ClusterIntegration|Environment scope')
= field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
.form-group .form-group
= field.submit _('Save changes'), class: 'btn btn-success' = field.submit _('Save changes'), class: 'btn btn-success'
- unless has_multiple_clusters?(@project) - unless has_multiple_clusters?
%h5= s_('ClusterIntegration|Environment scope') %h5= s_('ClusterIntegration|Environment scope')
%p %p
%code * %code *
......
...@@ -12,14 +12,14 @@ ...@@ -12,14 +12,14 @@
%p= link_to('Select a different Google account', @authorize_url) %p= link_to('Select a different Google account', @authorize_url)
= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: create_gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| = form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field|
= form_errors(@gcp_cluster) = form_errors(@gcp_cluster)
.form-group .form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group .form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold'
= field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?, placeholder: s_('ClusterIntegration|Environment scope')
= field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field| = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field|
.form-group .form-group
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%span.input-group-append %span.input-group-append
= clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default') = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default')
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
= form_errors(@cluster) = form_errors(@cluster)
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
......
...@@ -19,9 +19,9 @@ ...@@ -19,9 +19,9 @@
.tab-content.gitlab-tab-content .tab-content.gitlab-tab-content
.tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' } .tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' }
= render 'projects/clusters/gcp/header' = render 'clusters/clusters/gcp/header'
- if @valid_gcp_token - if @valid_gcp_token
= render 'projects/clusters/gcp/form' = render 'clusters/clusters/gcp/form'
- elsif @authorize_url - elsif @authorize_url
.signin-with-google .signin-with-google
= link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url)
...@@ -32,5 +32,5 @@ ...@@ -32,5 +32,5 @@
= s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link } = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
.tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' } .tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' }
= render 'projects/clusters/user/header' = render 'clusters/clusters/user/header'
= render 'projects/clusters/user/form' = render 'clusters/clusters/user/form'
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs "Kubernetes Clusters", project_clusters_path(@project) - add_to_breadcrumbs "Kubernetes Clusters", clusterable.index_path
- breadcrumb_title @cluster.name - breadcrumb_title @cluster.name
- page_title _("Kubernetes Cluster") - page_title _("Kubernetes Cluster")
- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
- expanded = Rails.env.test? - expanded = Rails.env.test?
- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) - status_path = clusterable.cluster_status_cluster_path(@cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster)
.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path, .edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm), install_helm_path: clusterable.install_applications_cluster_path(@cluster, :helm),
install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress), install_ingress_path: clusterable.install_applications_cluster_path(@cluster, :ingress),
install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus), install_prometheus_path: clusterable.install_applications_cluster_path(@cluster, :prometheus),
install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner), install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner),
install_jupyter_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :jupyter), install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
toggle_status: @cluster.enabled? ? 'true': 'false', toggle_status: @cluster.enabled? ? 'true': 'false',
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'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'), ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'), ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'),
manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } } manage_prometheus_path: manage_prometheus_path } }
.js-cluster-application-notice .js-cluster-application-notice
.flash-container .flash-container
...@@ -38,9 +39,9 @@ ...@@ -38,9 +39,9 @@
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster') %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content .settings-content
- if @cluster.managed? - if @cluster.managed?
= render 'projects/clusters/gcp/show' = render 'clusters/clusters/gcp/show'
- else - else
= render 'projects/clusters/user/show' = render 'clusters/clusters/user/show'
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) } %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
......
= form_for @user_cluster, url: create_user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| = form_for @user_cluster, url: clusterable.create_user_clusters_path, as: :cluster do |field|
= form_errors(@user_cluster) = form_errors(@user_cluster)
.form-group .form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
- if has_multiple_clusters?(@project) - if has_multiple_clusters?
.form-group .form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold'
= field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope')
......
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
= form_errors(@cluster) = form_errors(@cluster)
.form-group .form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
......
---
title: Change to top level controller for clusters so that we can use it for project
clusters (now) and group clusters (later)
merge_request: 22438
author:
type: other
...@@ -84,6 +84,23 @@ Rails.application.routes.draw do ...@@ -84,6 +84,23 @@ Rails.application.routes.draw do
draw :instance_statistics draw :instance_statistics
end end
concern :clusterable do
resources :clusters, only: [:index, :new, :show, :update, :destroy] do
collection do
post :create_user
post :create_gcp
end
member do
scope :applications do
post '/:application', to: 'clusters/applications#create', as: :install_applications
end
get :cluster_status, format: :json
end
end
end
draw :api draw :api
draw :sidekiq draw :sidekiq
draw :help draw :help
......
...@@ -206,20 +206,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -206,20 +206,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
end end
resources :clusters, except: [:edit, :create] do concerns :clusterable
collection do
post :create_gcp
post :create_user
end
member do
get :status, format: :json
scope :applications do
post '/:application', to: 'clusters/applications#create', as: :install_applications
end
end
end
resources :environments, except: [:destroy] do resources :environments, except: [:destroy] do
member do member do
......
...@@ -4,7 +4,7 @@ module QA ...@@ -4,7 +4,7 @@ module QA
module Operations module Operations
module Kubernetes module Kubernetes
class Add < Page::Base class Add < Page::Base
view 'app/views/projects/clusters/new.html.haml' do view 'app/views/clusters/clusters/new.html.haml' do
element :add_existing_cluster_button, "Add existing cluster" # rubocop:disable QA/ElementWithPattern element :add_existing_cluster_button, "Add existing cluster" # rubocop:disable QA/ElementWithPattern
end end
......
...@@ -4,7 +4,7 @@ module QA ...@@ -4,7 +4,7 @@ module QA
module Operations module Operations
module Kubernetes module Kubernetes
class AddExisting < Page::Base class AddExisting < Page::Base
view 'app/views/projects/clusters/user/_form.html.haml' do view 'app/views/clusters/clusters/user/_form.html.haml' do
element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern
element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern
......
...@@ -4,7 +4,7 @@ module QA ...@@ -4,7 +4,7 @@ module QA
module Operations module Operations
module Kubernetes module Kubernetes
class Index < Page::Base class Index < Page::Base
view 'app/views/projects/clusters/_empty_state.html.haml' do view 'app/views/clusters/clusters/_empty_state.html.haml' do
element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern
end end
......
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe Projects::Clusters::ApplicationsController do describe Projects::Clusters::ApplicationsController do
......
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe Projects::ClustersController do describe Projects::ClustersController do
...@@ -218,9 +220,9 @@ describe Projects::ClustersController do ...@@ -218,9 +220,9 @@ describe Projects::ClustersController do
describe 'security' do describe 'security' do
before do before do
allow_any_instance_of(described_class) allow_any_instance_of(described_class)
.to receive(:token_in_session).and_return('token') .to receive(:token_in_session).and_return('token')
allow_any_instance_of(described_class) allow_any_instance_of(described_class)
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
allow_any_instance_of(GoogleApi::CloudPlatform::Client) allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create) do .to receive(:projects_zones_clusters_create) do
OpenStruct.new( OpenStruct.new(
...@@ -318,14 +320,15 @@ describe Projects::ClustersController do ...@@ -318,14 +320,15 @@ describe Projects::ClustersController do
end end
end end
describe 'GET status' do describe 'GET cluster_status' do
let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) } let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
def go def go
get :status, namespace_id: project.namespace, get :cluster_status,
project_id: project, namespace_id: project.namespace.to_param,
id: cluster, project_id: project.to_param,
format: :json id: cluster,
format: :json
end end
describe 'functionality' do describe 'functionality' do
...@@ -359,9 +362,10 @@ describe Projects::ClustersController do ...@@ -359,9 +362,10 @@ describe Projects::ClustersController do
let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
def go def go
get :show, namespace_id: project.namespace, get :show,
project_id: project, namespace_id: project.namespace,
id: cluster project_id: project,
id: cluster
end end
describe 'functionality' do describe 'functionality' do
...@@ -401,8 +405,8 @@ describe Projects::ClustersController do ...@@ -401,8 +405,8 @@ describe Projects::ClustersController do
end end
def go(format: :html) def go(format: :html)
put :update, params.merge(namespace_id: project.namespace, put :update, params.merge(namespace_id: project.namespace.to_param,
project_id: project, project_id: project.to_param,
id: cluster, id: cluster,
format: format format: format
) )
...@@ -530,9 +534,10 @@ describe Projects::ClustersController do ...@@ -530,9 +534,10 @@ describe Projects::ClustersController do
let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) } let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
def go def go
delete :destroy, namespace_id: project.namespace, delete :destroy,
project_id: project, namespace_id: project.namespace,
id: cluster project_id: project,
id: cluster
end end
describe 'functionality' do describe 'functionality' do
...@@ -591,4 +596,10 @@ describe Projects::ClustersController do ...@@ -591,4 +596,10 @@ describe Projects::ClustersController do
it { expect { go }.to be_denied_for(:external) } it { expect { go }.to be_denied_for(:external) }
end end
end end
context 'no project_id param' do
it 'does not respond to any action without project_id param' do
expect { get :index }.to raise_error(ActionController::UrlGenerationError)
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe ClusterablePresenter do
include Gitlab::Routing.url_helpers
describe '.fabricate' do
let(:project) { create(:project) }
subject { described_class.fabricate(project) }
it 'creates an object from a descendant presenter' do
expect(subject).to be_kind_of(ProjectClusterablePresenter)
end
end
end
require 'spec_helper' require 'spec_helper'
describe Clusters::ClusterPresenter do describe Clusters::ClusterPresenter do
let(:cluster) { create(:cluster, :provided_by_gcp) } include Gitlab::Routing.url_helpers
let(:cluster) { create(:cluster, :provided_by_gcp, :project) }
subject(:presenter) do subject(:presenter) do
described_class.new(cluster) described_class.new(cluster)
...@@ -71,4 +73,14 @@ describe Clusters::ClusterPresenter do ...@@ -71,4 +73,14 @@ describe Clusters::ClusterPresenter do
it { is_expected.to eq(false) } it { is_expected.to eq(false) }
end end
end end
describe '#show_path' do
subject { described_class.new(cluster).show_path }
context 'project_type cluster' do
let(:project) { cluster.project }
it { is_expected.to eq(project_cluster_path(project, cluster)) }
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe ProjectClusterablePresenter do
include Gitlab::Routing.url_helpers
let(:presenter) { described_class.new(project) }
let(:project) { create(:project) }
let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
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
project.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(project_clusters_path(project)) }
end
describe '#new_path' do
subject { presenter.new_path }
it { is_expected.to eq(new_project_cluster_path(project)) }
end
describe '#create_user_clusters_path' do
subject { presenter.create_user_clusters_path }
it { is_expected.to eq(create_user_project_clusters_path(project)) }
end
describe '#create_gcp_clusters_path' do
subject { presenter.create_gcp_clusters_path }
it { is_expected.to eq(create_gcp_project_clusters_path(project)) }
end
describe '#cluster_status_cluster_path' do
subject { presenter.cluster_status_cluster_path(cluster) }
it { is_expected.to eq(cluster_status_project_cluster_path(project, 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_project_cluster_path(project, cluster, application)) }
end
describe '#cluster_path' do
subject { presenter.cluster_path(cluster) }
it { is_expected.to eq(project_cluster_path(project, cluster)) }
end
end
...@@ -5,18 +5,43 @@ describe Clusters::CreateService do ...@@ -5,18 +5,43 @@ describe Clusters::CreateService do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
subject { described_class.new(user, params).execute(project: project, access_token: access_token) } subject { described_class.new(user, params).execute(access_token: access_token) }
context 'when provider is gcp' do context 'when provider is gcp' do
context 'when project has no clusters' do context 'when project has no clusters' do
context 'when correct params' do context 'when correct params' do
include_context 'valid cluster create params' let(:params) do
{
name: 'test-cluster',
provider_type: :gcp,
provider_gcp_attributes: {
gcp_project_id: 'gcp-project',
zone: 'us-central1-a',
num_nodes: 1,
machine_type: 'machine_type-a',
legacy_abac: 'true'
},
clusterable: project
}
end
include_examples 'create cluster service success' include_examples 'create cluster service success'
end end
context 'when invalid params' do context 'when invalid params' do
include_context 'invalid cluster create params' let(:params) do
{
name: 'test-cluster',
provider_type: :gcp,
provider_gcp_attributes: {
gcp_project_id: '!!!!!!!',
zone: 'us-central1-a',
num_nodes: 1,
machine_type: 'machine_type-a'
},
clusterable: project
}
end
include_examples 'create cluster service error' include_examples 'create cluster service error'
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