Creates Clusterss::ApplciationsController update endpoint
- Creates new route - Creates new controller action - Creates call stack: Clusterss::ApplciationsController calls --> Clusters::Applications::UpdateService calls --> Clusters::Applications::ScheduleUpdateService calls --> ClusterUpdateAppWorker calls --> Clusters::Applications::PatchService --> ClusterWaitForAppInstallationWorker DRY req params Adds gcp_cluster:cluster_update_app queue Schedule_update_service is uneeded Extract common logic to a parent class (UpdateService will need it) Introduce new UpdateService Fix rescue class namespace Fix RuboCop offenses Adds BaseService for create and update services Remove request_handler code duplication Fixes update command Move update_command to ApplicationCore so all apps can use it Adds tests for Knative update_command Adds specs for PatchService Raise error if update receives an unistalled app Adds update_service spec Fix RuboCop offense Use subject in favor of go Adds update endpoint specs for project namespace Adds update endpoint specs for group namespace
This commit is contained in:
		
							parent
							
								
									cf1b85dd72
								
							
						
					
					
						commit
						f8234d9a08
					
				|  | @ -3,26 +3,41 @@ | ||||||
| class Clusters::ApplicationsController < Clusters::BaseController | class Clusters::ApplicationsController < Clusters::BaseController | ||||||
|   before_action :cluster |   before_action :cluster | ||||||
|   before_action :authorize_create_cluster!, only: [:create] |   before_action :authorize_create_cluster!, only: [:create] | ||||||
|  |   before_action :authorize_update_cluster!, only: [:update] | ||||||
| 
 | 
 | ||||||
|   def create |   def create | ||||||
|  |     request_handler do | ||||||
|       Clusters::Applications::CreateService |       Clusters::Applications::CreateService | ||||||
|       .new(@cluster, current_user, create_cluster_application_params) |         .new(@cluster, current_user, cluster_application_params) | ||||||
|         .execute(request) |         .execute(request) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def update | ||||||
|  |     request_handler do | ||||||
|  |       Clusters::Applications::UpdateService | ||||||
|  |         .new(@cluster, current_user, cluster_application_params) | ||||||
|  |         .execute(request) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def request_handler | ||||||
|  |     yield | ||||||
| 
 | 
 | ||||||
|     head :no_content |     head :no_content | ||||||
|   rescue Clusters::Applications::CreateService::InvalidApplicationError |   rescue Clusters::Applications::BaseService::InvalidApplicationError | ||||||
|     render_404 |     render_404 | ||||||
|   rescue StandardError |   rescue StandardError | ||||||
|     head :bad_request |     head :bad_request | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |  | ||||||
| 
 |  | ||||||
|   def cluster |   def cluster | ||||||
|     @cluster ||= clusterable.clusters.find(params[:id]) || render_404 |     @cluster ||= clusterable.clusters.find(params[:id]) || render_404 | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def create_cluster_application_params |   def cluster_application_params | ||||||
|     params.permit(:application, :hostname, :email) |     params.permit(:application, :hostname, :email) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -30,6 +30,12 @@ module Clusters | ||||||
|           # Override if you need extra data synchronized |           # Override if you need extra data synchronized | ||||||
|           # from K8s after installation |           # from K8s after installation | ||||||
|         end |         end | ||||||
|  | 
 | ||||||
|  |         def update_command | ||||||
|  |           command = install_command | ||||||
|  |           command.version = version | ||||||
|  |           command | ||||||
|  |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -46,6 +46,10 @@ module Clusters | ||||||
|         @install_command ||= app.install_command |         @install_command ||= app.install_command | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       def update_command | ||||||
|  |         @update_command ||= app.update_command | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       def upgrade_command(new_values = "") |       def upgrade_command(new_values = "") | ||||||
|         app.upgrade_command(new_values) |         app.upgrade_command(new_values) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,76 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module Clusters | ||||||
|  |   module Applications | ||||||
|  |     class BaseService | ||||||
|  |       InvalidApplicationError = Class.new(StandardError) | ||||||
|  | 
 | ||||||
|  |       attr_reader :cluster, :current_user, :params | ||||||
|  | 
 | ||||||
|  |       def initialize(cluster, user, params = {}) | ||||||
|  |         @cluster = cluster | ||||||
|  |         @current_user = user | ||||||
|  |         @params = params.dup | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def execute(request) | ||||||
|  |         instantiate_application.tap do |application| | ||||||
|  |           if application.has_attribute?(:hostname) | ||||||
|  |             application.hostname = params[:hostname] | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           if application.has_attribute?(:email) | ||||||
|  |             application.email = params[:email] | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           if application.respond_to?(:oauth_application) | ||||||
|  |             application.oauth_application = create_oauth_application(application, request) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           worker = worker_class(application) | ||||||
|  | 
 | ||||||
|  |           application.make_scheduled! | ||||||
|  | 
 | ||||||
|  |           worker.perform_async(application.name, application.id) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       protected | ||||||
|  | 
 | ||||||
|  |       def worker_class(application) | ||||||
|  |         raise NotImplementedError | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def builders | ||||||
|  |         raise NotImplementedError | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def project_builders | ||||||
|  |         raise NotImplementedError | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def instantiate_application | ||||||
|  |         builder.call(@cluster) || raise(InvalidApplicationError, "invalid application: #{application_name}") | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def builder | ||||||
|  |         builders[application_name] || raise(InvalidApplicationError, "invalid application: #{application_name}") | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def application_name | ||||||
|  |         params[:application] | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def create_oauth_application(application, request) | ||||||
|  |         oauth_application_params = { | ||||||
|  |           name: params[:application], | ||||||
|  |           redirect_uri: application.callback_url, | ||||||
|  |           scopes: 'api read_user openid', | ||||||
|  |           owner: current_user | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         ::Applications::CreateService.new(current_user, oauth_application_params).execute(request) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -2,47 +2,11 @@ | ||||||
| 
 | 
 | ||||||
| module Clusters | module Clusters | ||||||
|   module Applications |   module Applications | ||||||
|     class CreateService |     class CreateService < Clusters::Applications::BaseService | ||||||
|       InvalidApplicationError = Class.new(StandardError) |  | ||||||
| 
 |  | ||||||
|       attr_reader :cluster, :current_user, :params |  | ||||||
| 
 |  | ||||||
|       def initialize(cluster, user, params = {}) |  | ||||||
|         @cluster = cluster |  | ||||||
|         @current_user = user |  | ||||||
|         @params = params.dup |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       def execute(request) |  | ||||||
|         create_application.tap do |application| |  | ||||||
|           if application.has_attribute?(:hostname) |  | ||||||
|             application.hostname = params[:hostname] |  | ||||||
|           end |  | ||||||
| 
 |  | ||||||
|           if application.has_attribute?(:email) |  | ||||||
|             application.email = params[:email] |  | ||||||
|           end |  | ||||||
| 
 |  | ||||||
|           if application.respond_to?(:oauth_application) |  | ||||||
|             application.oauth_application = create_oauth_application(application, request) |  | ||||||
|           end |  | ||||||
| 
 |  | ||||||
|           worker = application.updateable? ? ClusterUpgradeAppWorker : ClusterInstallAppWorker |  | ||||||
| 
 |  | ||||||
|           application.make_scheduled! |  | ||||||
| 
 |  | ||||||
|           worker.perform_async(application.name, application.id) |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       private |       private | ||||||
| 
 | 
 | ||||||
|       def create_application |       def worker_class(application) | ||||||
|         builder.call(@cluster) |         application.updateable? ? ClusterUpgradeAppWorker : ClusterInstallAppWorker | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       def builder |  | ||||||
|         builders[application_name] || raise(InvalidApplicationError, "invalid application: #{application_name}") |  | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def builders |       def builders | ||||||
|  | @ -65,21 +29,6 @@ module Clusters | ||||||
|           "knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative } |           "knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative } | ||||||
|         } |         } | ||||||
|       end |       end | ||||||
| 
 |  | ||||||
|       def application_name |  | ||||||
|         params[:application] |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       def create_oauth_application(application, request) |  | ||||||
|         oauth_application_params = { |  | ||||||
|           name: params[:application], |  | ||||||
|           redirect_uri: application.callback_url, |  | ||||||
|           scopes: 'api read_user openid', |  | ||||||
|           owner: current_user |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         ::Applications::CreateService.new(current_user, oauth_application_params).execute(request) |  | ||||||
|       end |  | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module Clusters | ||||||
|  |   module Applications | ||||||
|  |     class PatchService < BaseHelmService | ||||||
|  |       def execute | ||||||
|  |         return unless app.scheduled? | ||||||
|  | 
 | ||||||
|  |         begin | ||||||
|  |           app.make_updating! | ||||||
|  | 
 | ||||||
|  |           helm_api.update(update_command) | ||||||
|  | 
 | ||||||
|  |           ClusterWaitForAppInstallationWorker.perform_in( | ||||||
|  |             ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) | ||||||
|  |         rescue Kubeclient::HttpError => e | ||||||
|  |           log_error(e) | ||||||
|  |           app.make_update_errored!("Kubernetes error: #{e.error_code}") | ||||||
|  |         rescue StandardError => e | ||||||
|  |           log_error(e) | ||||||
|  |           app.make_update_errored!("Can't start update process.") | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,34 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module Clusters | ||||||
|  |   module Applications | ||||||
|  |     class UpdateService < Clusters::Applications::BaseService | ||||||
|  |       private | ||||||
|  | 
 | ||||||
|  |       def worker_class(application) | ||||||
|  |         ClusterUpdateAppWorker | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def builders | ||||||
|  |         { | ||||||
|  |           "helm" => -> (cluster) { cluster.application_helm }, | ||||||
|  |           "ingress" => -> (cluster) { cluster.application_ingress }, | ||||||
|  |           "cert_manager" => -> (cluster) { cluster.application_cert_manager } | ||||||
|  |         }.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 }, | ||||||
|  |           "runner" => -> (cluster) { cluster.application_runner }, | ||||||
|  |           "jupyter" => -> (cluster) { cluster.application_jupyter }, | ||||||
|  |           "knative" => -> (cluster) { cluster.application_knative } | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -23,6 +23,7 @@ | ||||||
| - cronjob:prune_web_hook_logs | - cronjob:prune_web_hook_logs | ||||||
| 
 | 
 | ||||||
| - gcp_cluster:cluster_install_app | - gcp_cluster:cluster_install_app | ||||||
|  | - gcp_cluster:cluster_update_app | ||||||
| - gcp_cluster:cluster_upgrade_app | - gcp_cluster:cluster_upgrade_app | ||||||
| - gcp_cluster:cluster_provision | - gcp_cluster:cluster_provision | ||||||
| - gcp_cluster:cluster_wait_for_app_installation | - gcp_cluster:cluster_wait_for_app_installation | ||||||
|  |  | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class ClusterUpdateAppWorker | ||||||
|  |   include ApplicationWorker | ||||||
|  |   include ClusterQueue | ||||||
|  |   include ClusterApplications | ||||||
|  | 
 | ||||||
|  |   def perform(app_name, app_id) | ||||||
|  |     find_application(app_name, app_id) do |app| | ||||||
|  |       Clusters::Applications::PatchService.new(app).execute | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -101,6 +101,7 @@ Rails.application.routes.draw do | ||||||
|       member do |       member do | ||||||
|         scope :applications do |         scope :applications do | ||||||
|           post '/:application', to: 'clusters/applications#create', as: :install_applications |           post '/:application', to: 'clusters/applications#create', as: :install_applications | ||||||
|  |           patch '/:application', to: 'clusters/applications#update', as: :update_applications | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         get :cluster_status, format: :json |         get :cluster_status, format: :json | ||||||
|  |  | ||||||
|  | @ -7,7 +7,8 @@ module Gitlab | ||||||
|         include BaseCommand |         include BaseCommand | ||||||
|         include ClientCommand |         include ClientCommand | ||||||
| 
 | 
 | ||||||
|         attr_reader :name, :files, :chart, :version, :repository, :preinstall, :postinstall |         attr_reader :name, :files, :chart, :repository, :preinstall, :postinstall | ||||||
|  |         attr_accessor :version | ||||||
| 
 | 
 | ||||||
|         def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil, preinstall: nil, postinstall: nil) |         def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil, preinstall: nil, postinstall: nil) | ||||||
|           @name = name |           @name = name | ||||||
|  |  | ||||||
|  | @ -9,9 +9,25 @@ describe Groups::Clusters::ApplicationsController do | ||||||
|     Clusters::Cluster::APPLICATIONS[application] |     Clusters::Cluster::APPLICATIONS[application] | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe 'POST create' do |   shared_examples 'a secure endpoint' do | ||||||
|  |     it { expect { subject }.to be_allowed_for(:admin) } | ||||||
|  |     it { expect { subject }.to be_allowed_for(:owner).of(group) } | ||||||
|  |     it { expect { subject }.to be_allowed_for(:maintainer).of(group) } | ||||||
|  |     it { expect { subject }.to be_denied_for(:developer).of(group) } | ||||||
|  |     it { expect { subject }.to be_denied_for(:reporter).of(group) } | ||||||
|  |     it { expect { subject }.to be_denied_for(:guest).of(group) } | ||||||
|  |     it { expect { subject }.to be_denied_for(:user) } | ||||||
|  |     it { expect { subject }.to be_denied_for(:external) } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   let(:cluster) { create(:cluster, :group, :provided_by_gcp) } |   let(:cluster) { create(:cluster, :group, :provided_by_gcp) } | ||||||
|   let(:group) { cluster.group } |   let(:group) { cluster.group } | ||||||
|  | 
 | ||||||
|  |   describe 'POST create' do | ||||||
|  |     subject do | ||||||
|  |       post :create, params: params.merge(group_id: group) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     let(:application) { 'helm' } |     let(:application) { 'helm' } | ||||||
|     let(:params) { { application: application, id: cluster.id } } |     let(:params) { { application: application, id: cluster.id } } | ||||||
| 
 | 
 | ||||||
|  | @ -26,7 +42,7 @@ describe Groups::Clusters::ApplicationsController do | ||||||
|       it 'schedule an application installation' do |       it 'schedule an application installation' do | ||||||
|         expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once |         expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once | ||||||
| 
 | 
 | ||||||
|         expect { go }.to change { current_application.count } |         expect { subject }.to change { current_application.count } | ||||||
|         expect(response).to have_http_status(:no_content) |         expect(response).to have_http_status(:no_content) | ||||||
|         expect(cluster.application_helm).to be_scheduled |         expect(cluster.application_helm).to be_scheduled | ||||||
|       end |       end | ||||||
|  | @ -37,7 +53,7 @@ describe Groups::Clusters::ApplicationsController do | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'return 404' do |         it 'return 404' do | ||||||
|           expect { go }.not_to change { current_application.count } |           expect { subject }.not_to change { current_application.count } | ||||||
|           expect(response).to have_http_status(:not_found) |           expect(response).to have_http_status(:not_found) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  | @ -46,9 +62,7 @@ describe Groups::Clusters::ApplicationsController do | ||||||
|         let(:application) { 'unkwnown-app' } |         let(:application) { 'unkwnown-app' } | ||||||
| 
 | 
 | ||||||
|         it 'return 404' do |         it 'return 404' do | ||||||
|           go |           is_expected.to have_http_status(:not_found) | ||||||
| 
 |  | ||||||
|           expect(response).to have_http_status(:not_found) |  | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  | @ -58,9 +72,7 @@ describe Groups::Clusters::ApplicationsController do | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'returns 400' do |         it 'returns 400' do | ||||||
|           go |           is_expected.to have_http_status(:bad_request) | ||||||
| 
 |  | ||||||
|           expect(response).to have_http_status(:bad_request) |  | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | @ -70,18 +82,66 @@ describe Groups::Clusters::ApplicationsController do | ||||||
|         allow(ClusterInstallAppWorker).to receive(:perform_async) |         allow(ClusterInstallAppWorker).to receive(:perform_async) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it { expect { go }.to be_allowed_for(:admin) } |       it_behaves_like 'a secure endpoint' | ||||||
|       it { expect { go }.to be_allowed_for(:owner).of(group) } |     end | ||||||
|       it { expect { go }.to be_allowed_for(:maintainer).of(group) } |  | ||||||
|       it { expect { go }.to be_denied_for(:developer).of(group) } |  | ||||||
|       it { expect { go }.to be_denied_for(:reporter).of(group) } |  | ||||||
|       it { expect { go }.to be_denied_for(:guest).of(group) } |  | ||||||
|       it { expect { go }.to be_denied_for(:user) } |  | ||||||
|       it { expect { go }.to be_denied_for(:external) } |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|     def go |   describe 'PATCH update' do | ||||||
|       post :create, params: params.merge(group_id: group) |     subject do | ||||||
|  |       patch :update, params: params.merge(group_id: group) | ||||||
|  |     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(:user) { create(:user) } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         group.add_maintainer(user) | ||||||
|  |         sign_in(user) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context "when cluster and app exists" do | ||||||
|  |         it "schedules an application update" do | ||||||
|  |           expect(ClusterUpdateAppWorker).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(ClusterUpdateAppWorker).to receive(:perform_async) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it_behaves_like 'a secure endpoint' | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -9,7 +9,22 @@ describe Projects::Clusters::ApplicationsController do | ||||||
|     Clusters::Cluster::APPLICATIONS[application] |     Clusters::Cluster::APPLICATIONS[application] | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   shared_examples 'a secure endpoint' do | ||||||
|  |     it { expect { subject }.to be_allowed_for(:admin) } | ||||||
|  |     it { expect { subject }.to be_allowed_for(:owner).of(project) } | ||||||
|  |     it { expect { subject }.to be_allowed_for(:maintainer).of(project) } | ||||||
|  |     it { expect { subject }.to be_denied_for(:developer).of(project) } | ||||||
|  |     it { expect { subject }.to be_denied_for(:reporter).of(project) } | ||||||
|  |     it { expect { subject }.to be_denied_for(:guest).of(project) } | ||||||
|  |     it { expect { subject }.to be_denied_for(:user) } | ||||||
|  |     it { expect { subject }.to be_denied_for(:external) } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe 'POST create' do |   describe 'POST create' do | ||||||
|  |     subject do | ||||||
|  |       post :create, params: params.merge(namespace_id: project.namespace, project_id: project) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     let(:cluster) { create(:cluster, :project, :provided_by_gcp) } |     let(:cluster) { create(:cluster, :project, :provided_by_gcp) } | ||||||
|     let(:project) { cluster.project } |     let(:project) { cluster.project } | ||||||
|     let(:application) { 'helm' } |     let(:application) { 'helm' } | ||||||
|  | @ -26,7 +41,7 @@ describe Projects::Clusters::ApplicationsController do | ||||||
|       it 'schedule an application installation' do |       it 'schedule an application installation' do | ||||||
|         expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once |         expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once | ||||||
| 
 | 
 | ||||||
|         expect { go }.to change { current_application.count } |         expect { subject }.to change { current_application.count } | ||||||
|         expect(response).to have_http_status(:no_content) |         expect(response).to have_http_status(:no_content) | ||||||
|         expect(cluster.application_helm).to be_scheduled |         expect(cluster.application_helm).to be_scheduled | ||||||
|       end |       end | ||||||
|  | @ -37,7 +52,7 @@ describe Projects::Clusters::ApplicationsController do | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'return 404' do |         it 'return 404' do | ||||||
|           expect { go }.not_to change { current_application.count } |           expect { subject }.not_to change { current_application.count } | ||||||
|           expect(response).to have_http_status(:not_found) |           expect(response).to have_http_status(:not_found) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  | @ -46,9 +61,7 @@ describe Projects::Clusters::ApplicationsController do | ||||||
|         let(:application) { 'unkwnown-app' } |         let(:application) { 'unkwnown-app' } | ||||||
| 
 | 
 | ||||||
|         it 'return 404' do |         it 'return 404' do | ||||||
|           go |           is_expected.to have_http_status(:not_found) | ||||||
| 
 |  | ||||||
|           expect(response).to have_http_status(:not_found) |  | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  | @ -58,9 +71,7 @@ describe Projects::Clusters::ApplicationsController do | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'returns 400' do |         it 'returns 400' do | ||||||
|           go |           is_expected.to have_http_status(:bad_request) | ||||||
| 
 |  | ||||||
|           expect(response).to have_http_status(:bad_request) |  | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | @ -70,18 +81,68 @@ describe Projects::Clusters::ApplicationsController do | ||||||
|         allow(ClusterInstallAppWorker).to receive(:perform_async) |         allow(ClusterInstallAppWorker).to receive(:perform_async) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it { expect { go }.to be_allowed_for(:admin) } |       it_behaves_like 'a secure endpoint' | ||||||
|       it { expect { go }.to be_allowed_for(:owner).of(project) } |     end | ||||||
|       it { expect { go }.to be_allowed_for(:maintainer).of(project) } |  | ||||||
|       it { expect { go }.to be_denied_for(:developer).of(project) } |  | ||||||
|       it { expect { go }.to be_denied_for(:reporter).of(project) } |  | ||||||
|       it { expect { go }.to be_denied_for(:guest).of(project) } |  | ||||||
|       it { expect { go }.to be_denied_for(:user) } |  | ||||||
|       it { expect { go }.to be_denied_for(:external) } |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|     def go |   describe 'PATCH update' do | ||||||
|       post :create, params: params.merge(namespace_id: project.namespace, project_id: project) |     subject do | ||||||
|  |       patch :update, params: params.merge(namespace_id: project.namespace, project_id: project) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     let(:cluster) { create(:cluster, :project, :provided_by_gcp) } | ||||||
|  |     let(:project) { cluster.project } | ||||||
|  |     let!(:application) { create(:clusters_applications_knative, :installed, cluster: cluster) } | ||||||
|  |     let(:application_name) { application.name } | ||||||
|  |     let(:params) { { application: application_name, id: cluster.id, hostname: "new.example.com" } } | ||||||
|  | 
 | ||||||
|  |     describe 'functionality' do | ||||||
|  |       let(:user) { create(:user) } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         project.add_maintainer(user) | ||||||
|  |         sign_in(user) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context "when cluster and app exists" do | ||||||
|  |         it "schedules an application update" do | ||||||
|  |           expect(ClusterUpdateAppWorker).to receive(:perform_async).with(application.name, anything).once | ||||||
|  | 
 | ||||||
|  |           is_expected.to have_http_status(:no_content) | ||||||
|  | 
 | ||||||
|  |           expect(cluster.application_knative).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(ClusterUpdateAppWorker).to receive(:perform_async) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it_behaves_like 'a secure endpoint' | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -66,9 +66,7 @@ describe Clusters::Applications::Knative do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#install_command' do |   shared_examples 'a command' do | ||||||
|     subject { knative.install_command } |  | ||||||
| 
 |  | ||||||
|     it 'should be an instance of Helm::InstallCommand' do |     it 'should be an instance of Helm::InstallCommand' do | ||||||
|       expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) |       expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) | ||||||
|     end |     end | ||||||
|  | @ -76,7 +74,6 @@ describe Clusters::Applications::Knative do | ||||||
|     it 'should be initialized with knative arguments' do |     it 'should be initialized with knative arguments' do | ||||||
|       expect(subject.name).to eq('knative') |       expect(subject.name).to eq('knative') | ||||||
|       expect(subject.chart).to eq('knative/knative') |       expect(subject.chart).to eq('knative/knative') | ||||||
|       expect(subject.version).to eq('0.2.2') |  | ||||||
|       expect(subject.files).to eq(knative.files) |       expect(subject.files).to eq(knative.files) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -98,6 +95,27 @@ describe Clusters::Applications::Knative do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe '#install_command' do | ||||||
|  |     subject { knative.install_command } | ||||||
|  | 
 | ||||||
|  |     it 'should be initialized with latest version' do | ||||||
|  |       expect(subject.version).to eq('0.2.2') | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it_behaves_like 'a command' | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#update_command' do | ||||||
|  |     let!(:current_installed_version) { knative.version = '0.1.0' } | ||||||
|  |     subject { knative.update_command } | ||||||
|  | 
 | ||||||
|  |     it 'should be initialized with current version' do | ||||||
|  |       expect(subject.version).to eq(current_installed_version) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it_behaves_like 'a command' | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '#files' do |   describe '#files' do | ||||||
|     let(:application) { knative } |     let(:application) { knative } | ||||||
|     let(:values) { subject[:'values.yaml'] } |     let(:values) { subject[:'values.yaml'] } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,128 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | describe Clusters::Applications::PatchService do | ||||||
|  |   describe '#execute' do | ||||||
|  |     let(:application) { create(:clusters_applications_knative, :scheduled) } | ||||||
|  |     let!(:update_command) { application.update_command } | ||||||
|  |     let(:service) { described_class.new(application) } | ||||||
|  |     let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       allow(service).to receive(:update_command).and_return(update_command) | ||||||
|  |       allow(service).to receive(:helm_api).and_return(helm_client) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when there are no errors' do | ||||||
|  |       before do | ||||||
|  |         expect(helm_client).to receive(:update).with(update_command) | ||||||
|  |         allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'make the application updating' do | ||||||
|  |         expect(application.cluster).not_to be_nil | ||||||
|  |         service.execute | ||||||
|  | 
 | ||||||
|  |         expect(application).to be_updating | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'schedule async installation status check' do | ||||||
|  |         expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once | ||||||
|  | 
 | ||||||
|  |         service.execute | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when kubernetes cluster communication fails' do | ||||||
|  |       let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         expect(helm_client).to receive(:update).with(update_command).and_raise(error) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'make the application errored' do | ||||||
|  |         service.execute | ||||||
|  | 
 | ||||||
|  |         expect(application).to be_update_errored | ||||||
|  |         expect(application.status_reason).to match('Kubernetes error: 500') | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'logs errors' do | ||||||
|  |         expect(service.send(:logger)).to receive(:error).with( | ||||||
|  |           { | ||||||
|  |             exception: 'Kubeclient::HttpError', | ||||||
|  |             message: 'system failure', | ||||||
|  |             service: 'Clusters::Applications::PatchService', | ||||||
|  |             app_id: application.id, | ||||||
|  |             project_ids: application.cluster.project_ids, | ||||||
|  |             group_ids: [], | ||||||
|  |             error_code: 500 | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( | ||||||
|  |           error, | ||||||
|  |           extra: { | ||||||
|  |             exception: 'Kubeclient::HttpError', | ||||||
|  |             message: 'system failure', | ||||||
|  |             service: 'Clusters::Applications::PatchService', | ||||||
|  |             app_id: application.id, | ||||||
|  |             project_ids: application.cluster.project_ids, | ||||||
|  |             group_ids: [], | ||||||
|  |             error_code: 500 | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         service.execute | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'a non kubernetes error happens' do | ||||||
|  |       let(:application) { create(:clusters_applications_knative, :scheduled) } | ||||||
|  |       let(:error) { StandardError.new('something bad happened') } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         expect(application).to receive(:make_updating!).once.and_raise(error) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'make the application errored' do | ||||||
|  |         expect(helm_client).not_to receive(:update) | ||||||
|  | 
 | ||||||
|  |         service.execute | ||||||
|  | 
 | ||||||
|  |         expect(application).to be_update_errored | ||||||
|  |         expect(application.status_reason).to eq("Can't start update process.") | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'logs errors' do | ||||||
|  |         expect(service.send(:logger)).to receive(:error).with( | ||||||
|  |           { | ||||||
|  |             exception: 'StandardError', | ||||||
|  |             error_code: nil, | ||||||
|  |             message: 'something bad happened', | ||||||
|  |             service: 'Clusters::Applications::PatchService', | ||||||
|  |             app_id: application.id, | ||||||
|  |             project_ids: application.cluster.projects.pluck(:id), | ||||||
|  |             group_ids: [] | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( | ||||||
|  |           error, | ||||||
|  |           extra: { | ||||||
|  |             exception: 'StandardError', | ||||||
|  |             error_code: nil, | ||||||
|  |             message: 'something bad happened', | ||||||
|  |             service: 'Clusters::Applications::PatchService', | ||||||
|  |             app_id: application.id, | ||||||
|  |             project_ids: application.cluster.projects.pluck(:id), | ||||||
|  |             group_ids: [] | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         service.execute | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,72 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | describe Clusters::Applications::UpdateService do | ||||||
|  |   include TestRequestHelpers | ||||||
|  | 
 | ||||||
|  |   let(:cluster) { create(:cluster, :project, :provided_by_gcp) } | ||||||
|  |   let(:user) { create(:user) } | ||||||
|  |   let(:params) { { application: 'knative', hostname: 'udpate.example.com' } } | ||||||
|  |   let(:service) { described_class.new(cluster, user, params) } | ||||||
|  | 
 | ||||||
|  |   subject { service.execute(test_request) } | ||||||
|  | 
 | ||||||
|  |   describe '#execute' do | ||||||
|  |     before do | ||||||
|  |       allow(ClusterUpdateAppWorker).to receive(:perform_async) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'application is not installed' do | ||||||
|  |       it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do | ||||||
|  |         expect(ClusterUpdateAppWorker).not_to receive(:perform_async) | ||||||
|  | 
 | ||||||
|  |         expect { subject } | ||||||
|  |           .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError } | ||||||
|  |           .and not_change { Clusters::Applications::Knative.count } | ||||||
|  |           .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count } | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'application is installed' do | ||||||
|  |       context 'application is schedulable' do | ||||||
|  |         let!(:application) do | ||||||
|  |           create(:clusters_applications_knative, status: 3, cluster: cluster) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'updates the application data' do | ||||||
|  |           expect do | ||||||
|  |             subject | ||||||
|  |           end.to change { application.reload.hostname }.to(params[:hostname]) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'makes application scheduled!' do | ||||||
|  |           subject | ||||||
|  | 
 | ||||||
|  |           expect(application.reload).to be_scheduled | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'schedules ClusterUpdateAppWorker' do | ||||||
|  |           expect(ClusterUpdateAppWorker).to receive(:perform_async) | ||||||
|  | 
 | ||||||
|  |           subject | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'application is not schedulable' do | ||||||
|  |         let!(:application) do | ||||||
|  |           create(:clusters_applications_knative, status: 4, cluster: cluster) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'raises StateMachines::InvalidTransition' do | ||||||
|  |           expect(ClusterUpdateAppWorker).not_to receive(:perform_async) | ||||||
|  | 
 | ||||||
|  |           expect { subject } | ||||||
|  |             .to raise_exception { StateMachines::InvalidTransition } | ||||||
|  |             .and not_change { application.reload.hostname } | ||||||
|  |             .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count } | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
		Loading…
	
		Reference in New Issue