diff options
author | James Fargher <proglottis@gmail.com> | 2019-04-10 14:13:43 +1200 |
---|---|---|
committer | James Fargher <proglottis@gmail.com> | 2019-05-07 08:37:03 +1200 |
commit | 733da6d6a015e8c951dcc02250cfe1fab87789c0 (patch) | |
tree | 6be40311a4753767d7219f2cff21c4eccbe18a5f | |
parent | 863f2bcfb6ef7c6d3ce5726fa1a602e12f05ef57 (diff) | |
download | gitlab-ce-733da6d6a015e8c951dcc02250cfe1fab87789c0.tar.gz |
Instance level kubernetes clusters admin
Instance level clusters were already mostly supported, this change adds
admin area controllers for cluster CRUD
27 files changed, 969 insertions, 5 deletions
diff --git a/app/assets/javascripts/pages/admin/clusters/destroy/index.js b/app/assets/javascripts/pages/admin/clusters/destroy/index.js new file mode 100644 index 00000000000..8001d2dd1da --- /dev/null +++ b/app/assets/javascripts/pages/admin/clusters/destroy/index.js @@ -0,0 +1,5 @@ +import ClustersBundle from '~/clusters/clusters_bundle'; + +document.addEventListener('DOMContentLoaded', () => { + new ClustersBundle(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/admin/clusters/edit/index.js b/app/assets/javascripts/pages/admin/clusters/edit/index.js new file mode 100644 index 00000000000..8001d2dd1da --- /dev/null +++ b/app/assets/javascripts/pages/admin/clusters/edit/index.js @@ -0,0 +1,5 @@ +import ClustersBundle from '~/clusters/clusters_bundle'; + +document.addEventListener('DOMContentLoaded', () => { + new ClustersBundle(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/admin/clusters/index.js b/app/assets/javascripts/pages/admin/clusters/index.js new file mode 100644 index 00000000000..d0c9ae66c6a --- /dev/null +++ b/app/assets/javascripts/pages/admin/clusters/index.js @@ -0,0 +1,21 @@ +import PersistentUserCallout from '~/persistent_user_callout'; +import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; + +function initGcpSignupCallout() { + const callout = document.querySelector('.gcp-signup-offer'); + PersistentUserCallout.factory(callout); +} + +document.addEventListener('DOMContentLoaded', () => { + const { page } = document.body.dataset; + const newClusterViews = [ + 'admin:clusters:new', + 'admin:clusters:create_gcp', + 'admin:clusters:create_user', + ]; + + if (newClusterViews.indexOf(page) > -1) { + initGcpSignupCallout(); + initGkeDropdowns(); + } +}); diff --git a/app/assets/javascripts/pages/admin/clusters/index/index.js b/app/assets/javascripts/pages/admin/clusters/index/index.js new file mode 100644 index 00000000000..30d519d0e37 --- /dev/null +++ b/app/assets/javascripts/pages/admin/clusters/index/index.js @@ -0,0 +1,6 @@ +import PersistentUserCallout from '~/persistent_user_callout'; + +document.addEventListener('DOMContentLoaded', () => { + const callout = document.querySelector('.gcp-signup-offer'); + PersistentUserCallout.factory(callout); +}); diff --git a/app/assets/javascripts/pages/admin/clusters/show/index.js b/app/assets/javascripts/pages/admin/clusters/show/index.js new file mode 100644 index 00000000000..8001d2dd1da --- /dev/null +++ b/app/assets/javascripts/pages/admin/clusters/show/index.js @@ -0,0 +1,5 @@ +import ClustersBundle from '~/clusters/clusters_bundle'; + +document.addEventListener('DOMContentLoaded', () => { + new ClustersBundle(); // eslint-disable-line no-new +}); diff --git a/app/controllers/admin/clusters/applications_controller.rb b/app/controllers/admin/clusters/applications_controller.rb new file mode 100644 index 00000000000..3351d3ff825 --- /dev/null +++ b/app/controllers/admin/clusters/applications_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Clusters::ApplicationsController < Clusters::ApplicationsController + private + + def clusterable + @clusterable ||= InstanceClusterablePresenter.fabricate(Clusters::Instance.new, current_user: current_user) + end +end diff --git a/app/controllers/admin/clusters_controller.rb b/app/controllers/admin/clusters_controller.rb new file mode 100644 index 00000000000..777bdf5c981 --- /dev/null +++ b/app/controllers/admin/clusters_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Admin::ClustersController < Clusters::ClustersController + prepend_before_action :check_instance_clusters_feature_flag! + + layout 'admin' + + private + + def clusterable + @clusterable ||= InstanceClusterablePresenter.fabricate(Clusters::Instance.new, current_user: current_user) + end + + def check_instance_clusters_feature_flag! + render_404 unless Feature.enabled?(:instance_clusters, default_enabled: true) + end +end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index af648db3708..ceecd931bba 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -69,10 +69,12 @@ module Clusters } if cluster.group_type? - attributes.merge(groups: [group]) + attributes[:groups] = [group] elsif cluster.project_type? - attributes.merge(projects: [project]) + attributes[:projects] = [project] end + + attributes end def gitlab_url diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index d2b1adacbfb..7220159ac95 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -177,6 +177,10 @@ module Clusters end alias_method :group, :first_group + def instance + Instance.new if instance_type? + end + def kubeclient platform_kubernetes.kubeclient if kubernetes? end diff --git a/app/models/clusters/instance.rb b/app/models/clusters/instance.rb new file mode 100644 index 00000000000..fde83c5a8ad --- /dev/null +++ b/app/models/clusters/instance.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Clusters::Instance + def clusters + Clusters::Cluster.instance_type + end + + def feature_available?(feature) + ::Feature.enabled?(feature, default_enabled: true) + end +end diff --git a/app/policies/clusters/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb index d6d590687e2..316bd39f7a3 100644 --- a/app/policies/clusters/cluster_policy.rb +++ b/app/policies/clusters/cluster_policy.rb @@ -6,5 +6,6 @@ module Clusters delegate { cluster.group } delegate { cluster.project } + delegate { cluster.instance } end end diff --git a/app/policies/clusters/instance_policy.rb b/app/policies/clusters/instance_policy.rb new file mode 100644 index 00000000000..f72096e8fc6 --- /dev/null +++ b/app/policies/clusters/instance_policy.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Clusters + class InstancePolicy < BasePolicy + include ClusterableActions + + condition(:has_clusters, scope: :subject) { clusterable_has_clusters? } + condition(:can_have_multiple_clusters) { multiple_clusters_available? } + + rule { admin }.policy do + enable :read_cluster + enable :add_cluster + enable :create_cluster + enable :update_cluster + enable :admin_cluster + end + + rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster + end +end diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb index 81994bbce7d..33b217c8498 100644 --- a/app/presenters/clusters/cluster_presenter.rb +++ b/app/presenters/clusters/cluster_presenter.rb @@ -35,6 +35,8 @@ module Clusters s_("ClusterIntegration|Project cluster") elsif cluster.group_type? s_("ClusterIntegration|Group cluster") + elsif cluster.instance_type? + s_("ClusterIntegration|Instance cluster") end end @@ -43,6 +45,8 @@ module Clusters project_cluster_path(project, cluster) elsif cluster.group_type? group_cluster_path(group, cluster) + elsif cluster.instance_type? + admin_cluster_path(cluster) else raise NotImplementedError end diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb new file mode 100644 index 00000000000..f8bbe5216f1 --- /dev/null +++ b/app/presenters/instance_clusterable_presenter.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +class InstanceClusterablePresenter < ClusterablePresenter + extend ::Gitlab::Utils::Override + include ActionView::Helpers::UrlHelper + + def self.fabricate(clusterable, **attributes) + attributes_with_presenter_class = attributes.merge(presenter_class: InstanceClusterablePresenter) + + Gitlab::View::Presenter::Factory + .new(clusterable, attributes_with_presenter_class) + .fabricate! + end + + override :index_path + def index_path + admin_clusters_path + end + + override :new_path + def new_path + new_admin_cluster_path + end + + override :cluster_status_cluster_path + def cluster_status_cluster_path(cluster, params = {}) + cluster_status_admin_cluster_path(cluster, params) + end + + override :install_applications_cluster_path + def install_applications_cluster_path(cluster, application) + install_applications_admin_cluster_path(cluster, application) + end + + override :update_applications_cluster_path + def update_applications_cluster_path(cluster, application) + update_applications_admin_cluster_path(cluster, application) + end + + override :cluster_path + def cluster_path(cluster, params = {}) + admin_cluster_path(cluster, params) + end + + override :create_user_clusters_path + def create_user_clusters_path + create_user_admin_clusters_path + end + + override :create_gcp_clusters_path + def create_gcp_clusters_path + create_gcp_admin_clusters_path + end + + override :empty_state_help_text + def empty_state_help_text + s_('ClusterIntegration|Adding an integration will share the cluster across all projects.') + end + + override :sidebar_text + def sidebar_text + s_('ClusterIntegration|Adding a Kubernetes cluster will automatically share the cluster across all 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 instance Kubernetes clusters'), help_page_path('user/instance/clusters/index'), target: '_blank', rel: 'noopener noreferrer') + end +end diff --git a/app/services/clusters/build_service.rb b/app/services/clusters/build_service.rb index 8de73831164..b1ac5549e30 100644 --- a/app/services/clusters/build_service.rb +++ b/app/services/clusters/build_service.rb @@ -12,6 +12,8 @@ module Clusters cluster.cluster_type = :project_type when ::Group cluster.cluster_type = :group_type + when Instance + cluster.cluster_type = :instance_type else raise NotImplementedError end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index 5a9da053780..886e484caaf 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -38,6 +38,8 @@ module Clusters { cluster_type: :project_type, projects: [clusterable] } when ::Group { cluster_type: :group_type, groups: [clusterable] } + when Instance + { cluster_type: :instance_type } else raise NotImplementedError end diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index ece66d3180b..4e96b904ad1 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -132,6 +132,18 @@ = _('Abuse Reports') %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all)) + = nav_link(controller: :clusters) do + = link_to admin_clusters_path 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 admin_clusters_path do + %strong.fly-out-top-item-name + = _('Kubernetes') + - if akismet_enabled? = nav_link(controller: :spam_logs) do = link_to admin_spam_logs_path do diff --git a/changelogs/unreleased/instance_level_clusters.yml b/changelogs/unreleased/instance_level_clusters.yml new file mode 100644 index 00000000000..afd06a4e05f --- /dev/null +++ b/changelogs/unreleased/instance_level_clusters.yml @@ -0,0 +1,5 @@ +--- +title: Instance level kubernetes clusters +merge_request: 27196 +author: +type: added diff --git a/config/routes/admin.rb b/config/routes/admin.rb index a01003b6039..90d7f4a04d4 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -132,5 +132,7 @@ namespace :admin do end end + concerns :clusterable + root to: 'dashboard#index' end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 445984c7847..5529d330097 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2023,9 +2023,15 @@ 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 a Kubernetes cluster will automatically share the cluster across all 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|Adding an integration will share the cluster across all projects." +msgstr "" + msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration" msgstr "" @@ -2206,6 +2212,9 @@ msgstr "" msgid "ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}." msgstr "" +msgid "ClusterIntegration|Instance cluster" +msgstr "" + msgid "ClusterIntegration|Integrate Kubernetes cluster automation" msgstr "" @@ -2272,6 +2281,9 @@ msgstr "" msgid "ClusterIntegration|Learn more about group Kubernetes clusters" msgstr "" +msgid "ClusterIntegration|Learn more about instance Kubernetes clusters" +msgstr "" + msgid "ClusterIntegration|Let's Encrypt" msgstr "" diff --git a/spec/controllers/admin/clusters/applications_controller_spec.rb b/spec/controllers/admin/clusters/applications_controller_spec.rb new file mode 100644 index 00000000000..cf202d88acc --- /dev/null +++ b/spec/controllers/admin/clusters/applications_controller_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Admin::Clusters::ApplicationsController do + include AccessMatchersForController + + def current_application + Clusters::Cluster::APPLICATIONS[application] + end + + shared_examples 'a secure endpoint' do + it { expect { subject }.to be_allowed_for(:admin) } + it { expect { subject }.to be_denied_for(:user) } + it { expect { subject }.to be_denied_for(:external) } + end + + let(:cluster) { create(:cluster, :instance, :provided_by_gcp) } + + describe 'POST create' do + subject do + post :create, params: params + end + + let(:application) { 'helm' } + let(:params) { { application: application, id: cluster.id } } + + describe 'functionality' do + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + it 'schedule an application installation' do + expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once + + expect { subject }.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 { subject }.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 + is_expected.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 + is_expected.to have_http_status(:bad_request) + end + end + end + + describe 'security' do + before do + allow(ClusterInstallAppWorker).to receive(:perform_async) + end + + it_behaves_like 'a secure endpoint' + end + end + + describe 'PATCH update' do + subject do + patch :update, params: params + end + + let!(:application) { create(:clusters_applications_cert_managers, :installed, cluster: cluster) } + let(:application_name) { application.name } + let(:params) { { application: application_name, id: cluster.id, email: "new-email@example.com" } } + + describe 'functionality' do + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + context "when cluster and app exists" do + it "schedules an application update" do + expect(ClusterPatchAppWorker).to receive(:perform_async).with(application.name, anything).once + + is_expected.to have_http_status(:no_content) + + expect(cluster.application_cert_manager).to be_scheduled + end + end + + context 'when cluster do not exists' do + before do + cluster.destroy! + end + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is unknown' do + let(:application_name) { 'unkwnown-app' } + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is already scheduled' do + before do + application.make_scheduled! + end + + it { is_expected.to have_http_status(:bad_request) } + end + end + + describe 'security' do + before do + allow(ClusterPatchAppWorker).to receive(:perform_async) + end + + it_behaves_like 'a secure endpoint' + end + end +end diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb new file mode 100644 index 00000000000..c59fc93ae09 --- /dev/null +++ b/spec/controllers/admin/clusters_controller_spec.rb @@ -0,0 +1,540 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Admin::ClustersController do + include AccessMatchersForController + include GoogleApi::CloudPlatformHelpers + + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + describe 'GET index' do + def go(params = {}) + get :index, params: params + end + + context 'when feature flag is not enabled' do + before do + stub_feature_flags(instance_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(instance_clusters: true) + end + + describe 'functionality' do + context 'when instance has one or more clusters' do + let!(:enabled_cluster) do + create(:cluster, :provided_by_gcp, :instance) + end + + let!(:disabled_cluster) do + create(:cluster, :disabled, :provided_by_gcp, :production_environment, :instance) + 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) { Clusters::Cluster.instance_type.page.total_pages } + + before do + allow(Clusters::Cluster).to receive(:paginates_per).and_return(1) + create_list(:cluster, 2, :provided_by_gcp, :production_environment, :instance) + 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 instance does not have a cluster' do + 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, :instance) } + + it { expect { go }.to be_allowed_for(:admin) } + 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 + 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_admin_cluster_path) + 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_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: params + 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 = Clusters::Cluster.instance_type.first + + expect(response).to redirect_to(admin_cluster_path(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(Clusters::Cluster.instance_type.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_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: params + 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 = Clusters::Cluster.instance_type.first + + expect(response).to redirect_to(admin_cluster_path(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 = Clusters::Cluster.instance_type.first + + expect(response).to redirect_to(admin_cluster_path(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_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, :instance) } + + def go + get :cluster_status, + params: { + 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_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + end + + describe 'GET show' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + def go + get :show, + params: { + 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_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + end + + describe 'PUT update' do + def go(format: :html) + put :update, params: params.merge( + id: cluster, + format: format + ) + end + + let(:cluster) { create(:cluster, :provided_by_user, :instance) } + let(:domain) { 'test-domain.com' } + + let(:params) do + { + cluster: { + enabled: false, + name: 'my-new-cluster-name', + base_domain: domain + } + } + end + + it 'updates and redirects back to show page' do + go + + cluster.reload + expect(response).to redirect_to(admin_cluster_path(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.domain).to eq('test-domain.com') + end + + context 'when domain is invalid' do + let(:domain) { 'http://not-a-valid-domain' } + + it 'does not update cluster attributes' do + go + + cluster.reload + expect(response).to render_template(:show) + expect(cluster.name).not_to eq('my-new-cluster-name') + expect(cluster.domain).not_to eq('test-domain.com') + end + 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', + domain: domain + } + } + 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, :instance) } + + it { expect { go }.to be_allowed_for(:admin) } + 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, :instance) } + + def go + delete :destroy, + params: { + 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(admin_clusters_path) + 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, :instance) } + + 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(admin_clusters_path) + 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, :instance) } + + 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(admin_clusters_path) + 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, :instance) } + + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + end +end diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index bdc0cb8ed86..4f0cd0efe9c 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -69,8 +69,8 @@ describe Clusters::Applications::Runner do expect(values).to include('privileged: true') expect(values).to include('image: ubuntu:16.04') expect(values).to include('resources') - expect(values).to match(/runnerToken: '?#{ci_runner.token}/) - expect(values).to match(/gitlabUrl: '?#{Gitlab::Routing.url_helpers.root_url}/) + expect(values).to match(/runnerToken: '?#{Regexp.escape(ci_runner.token)}/) + expect(values).to match(/gitlabUrl: '?#{Regexp.escape(Gitlab::Routing.url_helpers.root_url)}/) end context 'without a runner' do @@ -83,7 +83,7 @@ describe Clusters::Applications::Runner do end it 'uses the new runner token' do - expect(values).to match(/runnerToken: '?#{runner.token}/) + expect(values).to match(/runnerToken: '?#{Regexp.escape(runner.token)}/) end end @@ -114,6 +114,18 @@ describe Clusters::Applications::Runner do expect(runner.groups).to eq [group] end end + + context 'instance cluster' do + let(:cluster) { create(:cluster, :with_installed_helm, :instance) } + + include_examples 'runner creation' + + it 'creates an instance runner' do + subject + + expect(runner).to be_instance_type + end + end end context 'with duplicated values on vendor/runner/values.yaml' do diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb index b2f0ca1bc30..cc3dde154dc 100644 --- a/spec/policies/clusters/cluster_policy_spec.rb +++ b/spec/policies/clusters/cluster_policy_spec.rb @@ -66,5 +66,21 @@ describe Clusters::ClusterPolicy, :models do it { expect(policy).to be_disallowed :admin_cluster } end end + + context 'instance cluster' do + let(:cluster) { create(:cluster, :instance) } + + context 'when user' do + it { expect(policy).to be_disallowed :update_cluster } + it { expect(policy).to be_disallowed :admin_cluster } + end + + context 'when admin' do + let(:user) { create(:admin) } + + it { expect(policy).to be_allowed :update_cluster } + it { expect(policy).to be_allowed :admin_cluster } + end + end end end diff --git a/spec/policies/clusters/instance_policy_spec.rb b/spec/policies/clusters/instance_policy_spec.rb new file mode 100644 index 00000000000..ac0f9da5d19 --- /dev/null +++ b/spec/policies/clusters/instance_policy_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::InstancePolicy do + let(:cluster) { create(:cluster, :instance) } + let(:user) { create(:user) } + let(:policy) { described_class.new(user, cluster) } + + describe 'rules' do + context 'when user' do + it { expect(policy).to be_disallowed :update_cluster } + it { expect(policy).to be_disallowed :admin_cluster } + end + + context 'when admin' do + let(:user) { create(:admin) } + + it { expect(policy).to be_allowed :update_cluster } + it { expect(policy).to be_allowed :admin_cluster } + end + end +end diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb index a9d786bc872..42701a5f8d1 100644 --- a/spec/presenters/clusters/cluster_presenter_spec.rb +++ b/spec/presenters/clusters/cluster_presenter_spec.rb @@ -210,6 +210,12 @@ describe Clusters::ClusterPresenter do it { is_expected.to eq('Group cluster') } end + + context 'instance_type cluster' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it { is_expected.to eq('Instance cluster') } + end end describe '#show_path' do @@ -227,6 +233,12 @@ describe Clusters::ClusterPresenter do it { is_expected.to eq(group_cluster_path(group, cluster)) } end + + context 'instance_type cluster' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it { is_expected.to eq(admin_cluster_path(cluster)) } + end end describe '#read_only_kubernetes_platform_fields?' do diff --git a/spec/services/clusters/build_service_spec.rb b/spec/services/clusters/build_service_spec.rb index da0cb42b3a1..f3e852726f4 100644 --- a/spec/services/clusters/build_service_spec.rb +++ b/spec/services/clusters/build_service_spec.rb @@ -21,5 +21,13 @@ describe Clusters::BuildService do is_expected.to be_group_type end end + + describe 'when cluster subject is an instance' do + let(:cluster_subject) { Clusters::Instance.new } + + it 'sets the cluster_type to instance_type' do + is_expected.to be_instance_type + end + end end end |