From 45b3bcc39631729ce573210ef6201c2cc59d7cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Fri, 16 Mar 2018 17:50:48 +0100 Subject: Add zones_list and machine_types_list to Google Cloud API client --- lib/google_api/cloud_platform/client.rb | 19 ++++++++++++++++++ spec/lib/google_api/cloud_platform/client_spec.rb | 24 +++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb index f30dd995695..f7a00974efc 100644 --- a/lib/google_api/cloud_platform/client.rb +++ b/lib/google_api/cloud_platform/client.rb @@ -1,3 +1,4 @@ +require 'google/apis/compute_v1' require 'google/apis/container_v1' require 'google/apis/cloudbilling_v1' require 'google/apis/cloudresourcemanager_v1' @@ -58,6 +59,24 @@ module GoogleApi service.get_project_billing_info("projects/#{project_id}", options: user_agent_header) end + def projects_zones_list(project_id) + service = Google::Apis::ComputeV1::ComputeService.new + service.authorization = access_token + + service.fetch_all do |token| + service.list_zones(project_id, page_token: token, options: user_agent_header) + end + end + + def projects_zones_machine_types_list(project_id, zone) + service = Google::Apis::ComputeV1::ComputeService.new + service.authorization = access_token + + service.fetch_all do |token| + service.list_machine_types(project_id, zone, page_token: token, options: user_agent_header) + end + end + def projects_zones_clusters_get(project_id, zone, cluster_id) service = Google::Apis::ContainerV1::ContainerService.new service.authorization = access_token diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb index db9d9158b29..040cbda9468 100644 --- a/spec/lib/google_api/cloud_platform/client_spec.rb +++ b/spec/lib/google_api/cloud_platform/client_spec.rb @@ -74,6 +74,30 @@ describe GoogleApi::CloudPlatform::Client do it { is_expected.to eq(billing_info) } end + describe '#projects_zones_list' do + subject { client.projects_zones_list('project') } + let(:zones) { double } + + before do + allow_any_instance_of(Google::Apis::ComputeV1::ComputeService) + .to receive(:fetch_all).and_return(zones) + end + + it { is_expected.to eq(zones) } + end + + describe '#projects_zones_machine_types_list' do + subject { client.projects_zones_machine_types_list('project', 'zone') } + let(:machine_types) { double } + + before do + allow_any_instance_of(Google::Apis::ComputeV1::ComputeService) + .to receive(:fetch_all).and_return(machine_types) + end + + it { is_expected.to eq(machine_types) } + end + describe '#projects_zones_clusters_get' do subject { client.projects_zones_clusters_get(spy, spy, spy) } let(:gke_cluster) { double } -- cgit v1.2.1 From e911a0f896c3699c69b27509b1b38d173f96c81a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Fri, 16 Mar 2018 18:41:51 +0100 Subject: Expose CloudPlatform::Client#projects_list as GcpController#list_projects --- .../projects/clusters/gcp_controller.rb | 9 ++++++- config/routes/project.rb | 1 + .../projects/clusters/gcp_controller_spec.rb | 30 ++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb index 6b0b22f8e73..7dfe819525f 100644 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -1,6 +1,6 @@ class Projects::Clusters::GcpController < Projects::ApplicationController before_action :authorize_read_cluster! - before_action :authorize_google_api, except: [:login] + before_action :authorize_google_api, except: [:login, :list_projects] before_action :authorize_google_project_billing, only: [:new, :create] before_action :authorize_create_cluster!, only: [:new, :create] before_action :verify_billing, only: [:create] @@ -35,6 +35,13 @@ class Projects::Clusters::GcpController < Projects::ApplicationController end end + def list_projects + projects = GoogleApi::CloudPlatform::Client.new(token_in_session, nil).projects_list + respond_to do |format| + format.json { render status: :ok, json: { projects: projects } } + end + end + private def verify_billing diff --git a/config/routes/project.rb b/config/routes/project.rb index c803737d40b..4cd11fe76e8 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -200,6 +200,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get '/gcp/new', to: 'clusters/gcp#new' get '/gcp/login', to: 'clusters/gcp#login' + get '/gcp/list_projects', to: 'clusters/gcp#list_projects' post '/gcp', to: 'clusters/gcp#create' end end diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index e14ba29fa70..98158e081f8 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -199,4 +199,34 @@ describe Projects::Clusters::GcpController do post :create, params.merge(namespace_id: project.namespace, project_id: project) end end + + describe 'GET list_projects' do + describe 'functionality' do + let(:user) { create(:user) } + let(:api_response) { [project_id: 'test-project-1234'] } + + before do + project.add_master(user) + sign_in(user) + + allow_any_instance_of(GoogleApi::CloudPlatform::Client).to receive(:projects_list).and_return(api_response) + end + + it 'calls the Google Cloud Platform projects_list' do + expect_any_instance_of(GoogleApi::CloudPlatform::Client).to receive(:projects_list) + + go + end + + it 'renders the response as json' do + go + + expect(response.body).to eq({ projects: api_response }.to_json) + end + end + + def go + get :list_projects, namespace_id: project.namespace, project_id: project, format: :json + end + end end -- cgit v1.2.1 From cffc8899b75b018c0b2b619e25450c8e81507035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Fri, 20 Apr 2018 16:33:20 +0200 Subject: Rename CheckGcpProjectBillingService to ListGcpProjectsService --- app/services/check_gcp_project_billing_service.rb | 11 -------- app/services/list_gcp_projects_service.rb | 12 ++++++++ app/workers/check_gcp_project_billing_worker.rb | 2 +- .../check_gcp_project_billing_service_spec.rb | 32 ---------------------- spec/services/list_gcp_projects_service_spec.rb | 32 ++++++++++++++++++++++ .../check_gcp_project_billing_worker_spec.rb | 14 +++++----- 6 files changed, 52 insertions(+), 51 deletions(-) delete mode 100644 app/services/check_gcp_project_billing_service.rb create mode 100644 app/services/list_gcp_projects_service.rb delete mode 100644 spec/services/check_gcp_project_billing_service_spec.rb create mode 100644 spec/services/list_gcp_projects_service_spec.rb diff --git a/app/services/check_gcp_project_billing_service.rb b/app/services/check_gcp_project_billing_service.rb deleted file mode 100644 index ea82b61b279..00000000000 --- a/app/services/check_gcp_project_billing_service.rb +++ /dev/null @@ -1,11 +0,0 @@ -class CheckGcpProjectBillingService - def execute(token) - client = GoogleApi::CloudPlatform::Client.new(token, nil) - client.projects_list.select do |project| - begin - client.projects_get_billing_info(project.project_id).billing_enabled - rescue - end - end - end -end diff --git a/app/services/list_gcp_projects_service.rb b/app/services/list_gcp_projects_service.rb new file mode 100644 index 00000000000..7f7b35fd48a --- /dev/null +++ b/app/services/list_gcp_projects_service.rb @@ -0,0 +1,12 @@ +class ListGcpProjectsService + def execute(token) + client = GoogleApi::CloudPlatform::Client.new(token, nil) + # Lists only projects with billing enabled + client.projects_list.select do |project| + begin + client.projects_get_billing_info(project.project_id).billing_enabled + rescue + end + end + end +end diff --git a/app/workers/check_gcp_project_billing_worker.rb b/app/workers/check_gcp_project_billing_worker.rb index 363f81590ab..dd35b2de50d 100644 --- a/app/workers/check_gcp_project_billing_worker.rb +++ b/app/workers/check_gcp_project_billing_worker.rb @@ -37,7 +37,7 @@ class CheckGcpProjectBillingWorker return unless token return unless try_obtain_lease_for(token) - billing_enabled_state = !CheckGcpProjectBillingService.new.execute(token).empty? + billing_enabled_state = !ListGcpProjectsService.new.execute(token).empty? update_billing_change_counter(self.class.get_billing_state(token), billing_enabled_state) self.class.set_billing_state(token, billing_enabled_state) end diff --git a/spec/services/check_gcp_project_billing_service_spec.rb b/spec/services/check_gcp_project_billing_service_spec.rb deleted file mode 100644 index 3e68d906e71..00000000000 --- a/spec/services/check_gcp_project_billing_service_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe CheckGcpProjectBillingService do - include GoogleApi::CloudPlatformHelpers - - let(:service) { described_class.new } - let(:project_id) { 'test-project-1234' } - - describe '#execute' do - before do - stub_cloud_platform_projects_list(project_id: project_id) - end - - subject { service.execute('bogustoken') } - - context 'google account has a billing enabled gcp project' do - before do - stub_cloud_platform_projects_get_billing_info(project_id, true) - end - - it { is_expected.to all(satisfy { |project| project.project_id == project_id }) } - end - - context 'google account does not have a billing enabled gcp project' do - before do - stub_cloud_platform_projects_get_billing_info(project_id, false) - end - - it { is_expected.to eq([]) } - end - end -end diff --git a/spec/services/list_gcp_projects_service_spec.rb b/spec/services/list_gcp_projects_service_spec.rb new file mode 100644 index 00000000000..f1957b155e7 --- /dev/null +++ b/spec/services/list_gcp_projects_service_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe ListGcpProjectsService do + include GoogleApi::CloudPlatformHelpers + + let(:service) { described_class.new } + let(:project_id) { 'test-project-1234' } + + describe '#execute' do + before do + stub_cloud_platform_projects_list(project_id: project_id) + end + + subject { service.execute('bogustoken') } + + context 'google account has a billing enabled gcp project' do + before do + stub_cloud_platform_projects_get_billing_info(project_id, true) + end + + it { is_expected.to all(satisfy { |project| project.project_id == project_id }) } + end + + context 'google account does not have a billing enabled gcp project' do + before do + stub_cloud_platform_projects_get_billing_info(project_id, false) + end + + it { is_expected.to eq([]) } + end + end +end diff --git a/spec/workers/check_gcp_project_billing_worker_spec.rb b/spec/workers/check_gcp_project_billing_worker_spec.rb index 526ecf75921..ea929fd5b01 100644 --- a/spec/workers/check_gcp_project_billing_worker_spec.rb +++ b/spec/workers/check_gcp_project_billing_worker_spec.rb @@ -22,13 +22,13 @@ describe CheckGcpProjectBillingWorker do end it 'calls the service' do - expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) + expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) subject end it 'stores billing status in redis' do - expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) + expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) expect(described_class).to receive(:set_billing_state).with(token, true) subject @@ -41,7 +41,7 @@ describe CheckGcpProjectBillingWorker do end it 'does not call the service' do - expect(CheckGcpProjectBillingService).not_to receive(:new) + expect(ListGcpProjectsService).not_to receive(:new) subject end @@ -54,7 +54,7 @@ describe CheckGcpProjectBillingWorker do end it 'does not call the service' do - expect(CheckGcpProjectBillingService).not_to receive(:new) + expect(ListGcpProjectsService).not_to receive(:new) subject end @@ -77,7 +77,7 @@ describe CheckGcpProjectBillingWorker do context 'when the current state is false' do before do - expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([]) + expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([]) end it 'increments the billing change counter' do @@ -89,7 +89,7 @@ describe CheckGcpProjectBillingWorker do context 'when the current state is true' do before do - expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) + expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) end it 'increments the billing change counter' do @@ -103,7 +103,7 @@ describe CheckGcpProjectBillingWorker do context 'when previous state was true' do before do expect(described_class).to receive(:get_billing_state).and_return(true) - expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) + expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) end it 'increment the billing change counter' do -- cgit v1.2.1 From 3b1612515ff291b6a97657dd53a8fed6d681d7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Fri, 20 Apr 2018 17:02:48 +0200 Subject: Rename CheckGcpProjectBillingWorker to ListGcpProjectsWorker --- .../projects/clusters/gcp_controller.rb | 6 +- app/workers/all_queues.yml | 2 +- app/workers/check_gcp_project_billing_worker.rb | 92 ---------------- app/workers/list_gcp_projects_worker.rb | 92 ++++++++++++++++ .../projects/clusters/gcp_controller_spec.rb | 2 +- .../check_gcp_project_billing_worker_spec.rb | 116 --------------------- spec/workers/list_gcp_projects_worker_spec.rb | 116 +++++++++++++++++++++ 7 files changed, 213 insertions(+), 213 deletions(-) delete mode 100644 app/workers/check_gcp_project_billing_worker.rb create mode 100644 app/workers/list_gcp_projects_worker.rb delete mode 100644 spec/workers/check_gcp_project_billing_worker_spec.rb create mode 100644 spec/workers/list_gcp_projects_worker_spec.rb diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb index 7dfe819525f..5409432e1ab 100644 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -83,12 +83,12 @@ class Projects::Clusters::GcpController < Projects::ApplicationController end def authorize_google_project_billing - redis_token_key = CheckGcpProjectBillingWorker.store_session_token(token_in_session) - CheckGcpProjectBillingWorker.perform_async(redis_token_key) + redis_token_key = ListGcpProjectsWorker.store_session_token(token_in_session) + ListGcpProjectsWorker.perform_async(redis_token_key) end def google_project_billing_status - CheckGcpProjectBillingWorker.get_billing_state(token_in_session) + ListGcpProjectsWorker.get_billing_state(token_in_session) end def token_in_session diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 9aea3bad27b..1edfaa1691a 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -24,7 +24,7 @@ - gcp_cluster:cluster_provision - gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:wait_for_cluster_creation -- gcp_cluster:check_gcp_project_billing +- gcp_cluster:list_gcp_projects - gcp_cluster:cluster_wait_for_ingress_ip_address - github_import_advance_stage diff --git a/app/workers/check_gcp_project_billing_worker.rb b/app/workers/check_gcp_project_billing_worker.rb deleted file mode 100644 index dd35b2de50d..00000000000 --- a/app/workers/check_gcp_project_billing_worker.rb +++ /dev/null @@ -1,92 +0,0 @@ -require 'securerandom' - -class CheckGcpProjectBillingWorker - include ApplicationWorker - include ClusterQueue - - LEASE_TIMEOUT = 3.seconds.to_i - SESSION_KEY_TIMEOUT = 5.minutes - BILLING_TIMEOUT = 1.hour - BILLING_CHANGED_LABELS = { state_transition: nil }.freeze - - def self.get_session_token(token_key) - Gitlab::Redis::SharedState.with do |redis| - redis.get(get_redis_session_key(token_key)) - end - end - - def self.store_session_token(token) - generate_token_key.tap do |token_key| - Gitlab::Redis::SharedState.with do |redis| - redis.set(get_redis_session_key(token_key), token, ex: SESSION_KEY_TIMEOUT) - end - end - end - - def self.get_billing_state(token) - Gitlab::Redis::SharedState.with do |redis| - value = redis.get(redis_shared_state_key_for(token)) - ActiveRecord::Type::Boolean.new.type_cast_from_user(value) - end - end - - def perform(token_key) - return unless token_key - - token = self.class.get_session_token(token_key) - return unless token - return unless try_obtain_lease_for(token) - - billing_enabled_state = !ListGcpProjectsService.new.execute(token).empty? - update_billing_change_counter(self.class.get_billing_state(token), billing_enabled_state) - self.class.set_billing_state(token, billing_enabled_state) - end - - private - - def self.generate_token_key - SecureRandom.uuid - end - - def self.get_redis_session_key(token_key) - "gitlab:gcp:session:#{token_key}" - end - - def self.redis_shared_state_key_for(token) - "gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled" - end - - def self.set_billing_state(token, value) - Gitlab::Redis::SharedState.with do |redis| - redis.set(redis_shared_state_key_for(token), value, ex: BILLING_TIMEOUT) - end - end - - def try_obtain_lease_for(token) - Gitlab::ExclusiveLease - .new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT) - .try_obtain - end - - def billing_changed_counter - @billing_changed_counter ||= Gitlab::Metrics.counter( - :gcp_billing_change_count, - "Counts the number of times a GCP project changed billing_enabled state from false to true", - BILLING_CHANGED_LABELS - ) - end - - def state_transition(previous_state, current_state) - if previous_state.nil? && !current_state - 'no_billing' - elsif previous_state.nil? && current_state - 'with_billing' - elsif !previous_state && current_state - 'billing_configured' - end - end - - def update_billing_change_counter(previous_state, current_state) - billing_changed_counter.increment(state_transition: state_transition(previous_state, current_state)) - end -end diff --git a/app/workers/list_gcp_projects_worker.rb b/app/workers/list_gcp_projects_worker.rb new file mode 100644 index 00000000000..12cb5f738e4 --- /dev/null +++ b/app/workers/list_gcp_projects_worker.rb @@ -0,0 +1,92 @@ +require 'securerandom' + +class ListGcpProjectsWorker + include ApplicationWorker + include ClusterQueue + + LEASE_TIMEOUT = 3.seconds.to_i + SESSION_KEY_TIMEOUT = 5.minutes + BILLING_TIMEOUT = 1.hour + BILLING_CHANGED_LABELS = { state_transition: nil }.freeze + + def self.get_session_token(token_key) + Gitlab::Redis::SharedState.with do |redis| + redis.get(get_redis_session_key(token_key)) + end + end + + def self.store_session_token(token) + generate_token_key.tap do |token_key| + Gitlab::Redis::SharedState.with do |redis| + redis.set(get_redis_session_key(token_key), token, ex: SESSION_KEY_TIMEOUT) + end + end + end + + def self.get_billing_state(token) + Gitlab::Redis::SharedState.with do |redis| + value = redis.get(redis_shared_state_key_for(token)) + ActiveRecord::Type::Boolean.new.type_cast_from_user(value) + end + end + + def perform(token_key) + return unless token_key + + token = self.class.get_session_token(token_key) + return unless token + return unless try_obtain_lease_for(token) + + billing_enabled_state = !ListGcpProjectsService.new.execute(token).empty? + update_billing_change_counter(self.class.get_billing_state(token), billing_enabled_state) + self.class.set_billing_state(token, billing_enabled_state) + end + + private + + def self.generate_token_key + SecureRandom.uuid + end + + def self.get_redis_session_key(token_key) + "gitlab:gcp:session:#{token_key}" + end + + def self.redis_shared_state_key_for(token) + "gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled" + end + + def self.set_billing_state(token, value) + Gitlab::Redis::SharedState.with do |redis| + redis.set(redis_shared_state_key_for(token), value, ex: BILLING_TIMEOUT) + end + end + + def try_obtain_lease_for(token) + Gitlab::ExclusiveLease + .new("list_gcp_projects_worker:#{token.hash}", timeout: LEASE_TIMEOUT) + .try_obtain + end + + def billing_changed_counter + @billing_changed_counter ||= Gitlab::Metrics.counter( + :gcp_billing_change_count, + "Counts the number of times a GCP project changed billing_enabled state from false to true", + BILLING_CHANGED_LABELS + ) + end + + def state_transition(previous_state, current_state) + if previous_state.nil? && !current_state + 'no_billing' + elsif previous_state.nil? && current_state + 'with_billing' + elsif !previous_state && current_state + 'billing_configured' + end + end + + def update_billing_change_counter(previous_state, current_state) + billing_changed_counter.increment(state_transition: state_transition(previous_state, current_state)) + end +end diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index 98158e081f8..3051f9cfde4 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -144,7 +144,7 @@ describe Projects::Clusters::GcpController do before do redis_double = double allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double) - allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true') + allow(redis_double).to receive(:get).with(ListGcpProjectsWorker.redis_shared_state_key_for('token')).and_return('true') end it 'creates a new cluster' do diff --git a/spec/workers/check_gcp_project_billing_worker_spec.rb b/spec/workers/check_gcp_project_billing_worker_spec.rb deleted file mode 100644 index ea929fd5b01..00000000000 --- a/spec/workers/check_gcp_project_billing_worker_spec.rb +++ /dev/null @@ -1,116 +0,0 @@ -require 'spec_helper' - -describe CheckGcpProjectBillingWorker do - describe '.perform' do - let(:token) { 'bogustoken' } - - subject { described_class.new.perform('token_key') } - - before do - allow(described_class).to receive(:get_billing_state) - allow_any_instance_of(described_class).to receive(:update_billing_change_counter) - end - - context 'when there is a token in redis' do - before do - allow(described_class).to receive(:get_session_token).and_return(token) - end - - context 'when there is no lease' do - before do - allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid') - end - - it 'calls the service' do - expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) - - subject - end - - it 'stores billing status in redis' do - expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) - expect(described_class).to receive(:set_billing_state).with(token, true) - - subject - end - end - - context 'when there is a lease' do - before do - allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return(false) - end - - it 'does not call the service' do - expect(ListGcpProjectsService).not_to receive(:new) - - subject - end - end - end - - context 'when there is no token in redis' do - before do - allow(described_class).to receive(:get_session_token).and_return(nil) - end - - it 'does not call the service' do - expect(ListGcpProjectsService).not_to receive(:new) - - subject - end - end - end - - describe 'billing change counter' do - subject { described_class.new.perform('token_key') } - - before do - allow(described_class).to receive(:get_session_token).and_return('bogustoken') - allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid') - allow(described_class).to receive(:set_billing_state) - end - - context 'when previous state was false' do - before do - expect(described_class).to receive(:get_billing_state).and_return(false) - end - - context 'when the current state is false' do - before do - expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([]) - end - - it 'increments the billing change counter' do - expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) - - subject - end - end - - context 'when the current state is true' do - before do - expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) - end - - it 'increments the billing change counter' do - expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) - - subject - end - end - end - - context 'when previous state was true' do - before do - expect(described_class).to receive(:get_billing_state).and_return(true) - expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) - end - - it 'increment the billing change counter' do - expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) - - subject - end - end - end -end diff --git a/spec/workers/list_gcp_projects_worker_spec.rb b/spec/workers/list_gcp_projects_worker_spec.rb new file mode 100644 index 00000000000..ea94883b9a0 --- /dev/null +++ b/spec/workers/list_gcp_projects_worker_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' + +describe ListGcpProjectsWorker do + describe '.perform' do + let(:token) { 'bogustoken' } + + subject { described_class.new.perform('token_key') } + + before do + allow(described_class).to receive(:get_billing_state) + allow_any_instance_of(described_class).to receive(:update_billing_change_counter) + end + + context 'when there is a token in redis' do + before do + allow(described_class).to receive(:get_session_token).and_return(token) + end + + context 'when there is no lease' do + before do + allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid') + end + + it 'calls the service' do + expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) + + subject + end + + it 'stores billing status in redis' do + expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) + expect(described_class).to receive(:set_billing_state).with(token, true) + + subject + end + end + + context 'when there is a lease' do + before do + allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return(false) + end + + it 'does not call the service' do + expect(ListGcpProjectsService).not_to receive(:new) + + subject + end + end + end + + context 'when there is no token in redis' do + before do + allow(described_class).to receive(:get_session_token).and_return(nil) + end + + it 'does not call the service' do + expect(ListGcpProjectsService).not_to receive(:new) + + subject + end + end + end + + describe 'billing change counter' do + subject { described_class.new.perform('token_key') } + + before do + allow(described_class).to receive(:get_session_token).and_return('bogustoken') + allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid') + allow(described_class).to receive(:set_billing_state) + end + + context 'when previous state was false' do + before do + expect(described_class).to receive(:get_billing_state).and_return(false) + end + + context 'when the current state is false' do + before do + expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([]) + end + + it 'increments the billing change counter' do + expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) + + subject + end + end + + context 'when the current state is true' do + before do + expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) + end + + it 'increments the billing change counter' do + expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) + + subject + end + end + end + + context 'when previous state was true' do + before do + expect(described_class).to receive(:get_billing_state).and_return(true) + expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) + end + + it 'increment the billing change counter' do + expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) + + subject + end + end + end +end -- cgit v1.2.1 From e3176c201420560ba5e2d1963bc28d08e1637b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Fri, 20 Apr 2018 19:08:38 +0200 Subject: Store projects in ListGcpProjectsWorker --- .../projects/clusters/gcp_controller.rb | 19 ++++++++--------- app/workers/list_gcp_projects_worker.rb | 18 ++++++++-------- .../projects/clusters/gcp_controller_spec.rb | 24 +++++++++------------- spec/features/projects/clusters/gcp_spec.rb | 12 +++++------ spec/workers/list_gcp_projects_worker_spec.rb | 22 ++++++++++---------- 5 files changed, 45 insertions(+), 50 deletions(-) diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb index 5409432e1ab..2d03c1f9f85 100644 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -1,8 +1,8 @@ class Projects::Clusters::GcpController < Projects::ApplicationController before_action :authorize_read_cluster! - before_action :authorize_google_api, except: [:login, :list_projects] - before_action :authorize_google_project_billing, only: [:new, :create] before_action :authorize_create_cluster!, only: [:new, :create] + before_action :authorize_google_api, except: [:login, :list_projects] + before_action :get_gcp_projects, only: [:new, :create] before_action :verify_billing, only: [:create] def login @@ -36,21 +36,20 @@ class Projects::Clusters::GcpController < Projects::ApplicationController end def list_projects - projects = GoogleApi::CloudPlatform::Client.new(token_in_session, nil).projects_list respond_to do |format| - format.json { render status: :ok, json: { projects: projects } } + format.json { render status: :ok, json: { projects: gcp_projects } } end end private def verify_billing - case google_project_billing_status + case gcp_projects&.empty? when nil flash.now[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') - when false - flash.now[:alert] = _('Please enable billing for one of your projects to be able to create a Kubernetes cluster, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" } when true + flash.now[:alert] = _('Please enable billing for one of your projects to be able to create a Kubernetes cluster, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" } + when false return end @@ -82,13 +81,13 @@ class Projects::Clusters::GcpController < Projects::ApplicationController end end - def authorize_google_project_billing + def get_gcp_projects redis_token_key = ListGcpProjectsWorker.store_session_token(token_in_session) ListGcpProjectsWorker.perform_async(redis_token_key) end - def google_project_billing_status - ListGcpProjectsWorker.get_billing_state(token_in_session) + def gcp_projects + ListGcpProjectsWorker.get_projects(token_in_session) end def token_in_session diff --git a/app/workers/list_gcp_projects_worker.rb b/app/workers/list_gcp_projects_worker.rb index 12cb5f738e4..690980a4c4e 100644 --- a/app/workers/list_gcp_projects_worker.rb +++ b/app/workers/list_gcp_projects_worker.rb @@ -6,7 +6,7 @@ class ListGcpProjectsWorker LEASE_TIMEOUT = 3.seconds.to_i SESSION_KEY_TIMEOUT = 5.minutes - BILLING_TIMEOUT = 1.hour + PROJECT_TIMEOUT = 1.hour BILLING_CHANGED_LABELS = { state_transition: nil }.freeze def self.get_session_token(token_key) @@ -23,10 +23,10 @@ class ListGcpProjectsWorker end end - def self.get_billing_state(token) + def self.read_projects(token) Gitlab::Redis::SharedState.with do |redis| value = redis.get(redis_shared_state_key_for(token)) - ActiveRecord::Type::Boolean.new.type_cast_from_user(value) + JSON.parse(value) end end @@ -37,9 +37,9 @@ class ListGcpProjectsWorker return unless token return unless try_obtain_lease_for(token) - billing_enabled_state = !ListGcpProjectsService.new.execute(token).empty? - update_billing_change_counter(self.class.get_billing_state(token), billing_enabled_state) - self.class.set_billing_state(token, billing_enabled_state) + billing_enabled_projects = ListGcpProjectsService.new.execute(token) + update_billing_change_counter(!self.class.read_projects(token).empty?, !billing_enabled_projects.empty?) + self.class.store_projects(token, billing_enabled_projects.to_json) end private @@ -53,12 +53,12 @@ class ListGcpProjectsWorker end def self.redis_shared_state_key_for(token) - "gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled" + "gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:projects" end - def self.set_billing_state(token, value) + def self.store_projects(token, value) Gitlab::Redis::SharedState.with do |redis| - redis.set(redis_shared_state_key_for(token), value, ex: BILLING_TIMEOUT) + redis.set(redis_shared_state_key_for(token), value, ex: PROJECT_TIMEOUT) end end diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index 3051f9cfde4..8b72c1a8cd9 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -77,7 +77,7 @@ describe Projects::Clusters::GcpController do end it 'has new object' do - expect(controller).to receive(:authorize_google_project_billing) + expect(controller).to receive(:get_gcp_projects) go @@ -137,14 +137,12 @@ describe Projects::Clusters::GcpController do context 'when access token is valid' do before do stub_google_api_validate_token - allow_any_instance_of(described_class).to receive(:authorize_google_project_billing) + allow_any_instance_of(described_class).to receive(:get_gcp_projects) end context 'when google project billing is enabled' do before do - redis_double = double - allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double) - allow(redis_double).to receive(:get).with(ListGcpProjectsWorker.redis_shared_state_key_for('token')).and_return('true') + allow_any_instance_of(described_class).to receive(:gcp_projects).and_return([double]) end it 'creates a new cluster' do @@ -158,6 +156,10 @@ describe Projects::Clusters::GcpController do end context 'when google project billing is not enabled' do + before do + allow_any_instance_of(described_class).to receive(:gcp_projects).and_return([]) + end + it 'renders the cluster form with an error' do go @@ -203,25 +205,19 @@ describe Projects::Clusters::GcpController do describe 'GET list_projects' do describe 'functionality' do let(:user) { create(:user) } - let(:api_response) { [project_id: 'test-project-1234'] } + let(:gcp_projects) { [project_id: 'test-project-1234'] } before do project.add_master(user) sign_in(user) - allow_any_instance_of(GoogleApi::CloudPlatform::Client).to receive(:projects_list).and_return(api_response) - end - - it 'calls the Google Cloud Platform projects_list' do - expect_any_instance_of(GoogleApi::CloudPlatform::Client).to receive(:projects_list) - - go + allow_any_instance_of(described_class).to receive(:gcp_projects).and_return(gcp_projects) end it 'renders the response as json' do go - expect(response.body).to eq({ projects: api_response }.to_json) + expect(response.body).to eq({ projects: gcp_projects }.to_json) end end diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index dfe8e02dce0..3937fed490c 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -24,8 +24,8 @@ feature 'Gcp Cluster', :js do context 'when user has a GCP project with billing enabled' do before do - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(true) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:get_gcp_projects) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:gcp_projects).and_return([double]) end context 'when user does not have a cluster and visits cluster index page' do @@ -133,8 +133,8 @@ feature 'Gcp Cluster', :js do context 'when user does not have a GCP project with billing enabled' do before do - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(false) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:get_gcp_projects) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:gcp_projects).and_return([]) visit project_clusters_path(project) @@ -153,8 +153,8 @@ feature 'Gcp Cluster', :js do context 'when gcp billing status is not in redis' do before do - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(nil) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:get_gcp_projects) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:gcp_projects).and_return(nil) visit project_clusters_path(project) diff --git a/spec/workers/list_gcp_projects_worker_spec.rb b/spec/workers/list_gcp_projects_worker_spec.rb index ea94883b9a0..c415caf78f0 100644 --- a/spec/workers/list_gcp_projects_worker_spec.rb +++ b/spec/workers/list_gcp_projects_worker_spec.rb @@ -7,7 +7,7 @@ describe ListGcpProjectsWorker do subject { described_class.new.perform('token_key') } before do - allow(described_class).to receive(:get_billing_state) + allow(described_class).to receive(:read_projects).and_return([double]) allow_any_instance_of(described_class).to receive(:update_billing_change_counter) end @@ -27,9 +27,9 @@ describe ListGcpProjectsWorker do subject end - it 'stores billing status in redis' do + it 'stores projects in redis' do expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) - expect(described_class).to receive(:set_billing_state).with(token, true) + expect(described_class).to receive(:store_projects).with(token, anything) subject end @@ -67,15 +67,15 @@ describe ListGcpProjectsWorker do before do allow(described_class).to receive(:get_session_token).and_return('bogustoken') allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid') - allow(described_class).to receive(:set_billing_state) + allow(described_class).to receive(:store_projects) end - context 'when previous state was false' do + context 'when there are no projects in redis' do before do - expect(described_class).to receive(:get_billing_state).and_return(false) + expect(described_class).to receive(:read_projects).and_return([]) end - context 'when the current state is false' do + context 'when the service does not return projects' do before do expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([]) end @@ -87,7 +87,7 @@ describe ListGcpProjectsWorker do end end - context 'when the current state is true' do + context 'when the service returns projects' do before do expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) end @@ -100,13 +100,13 @@ describe ListGcpProjectsWorker do end end - context 'when previous state was true' do + context 'when there are projects in redis' do before do - expect(described_class).to receive(:get_billing_state).and_return(true) + expect(described_class).to receive(:read_projects).and_return([double]) expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) end - it 'increment the billing change counter' do + it 'increments the billing change counter' do expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) subject -- cgit v1.2.1 From 45dadeec7d9f7b527cf4bc25800dcdbd13366ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Fri, 20 Apr 2018 21:58:53 +0200 Subject: Remove the GCP billing verification step --- .../projects/clusters/gcp_controller.rb | 18 +-- .../projects/clusters/gcp_controller_spec.rb | 33 ++-- spec/features/projects/clusters/gcp_spec.rb | 180 ++++++++------------- 3 files changed, 77 insertions(+), 154 deletions(-) diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb index 2d03c1f9f85..585f30cd7f8 100644 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -2,8 +2,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController before_action :authorize_read_cluster! before_action :authorize_create_cluster!, only: [:new, :create] before_action :authorize_google_api, except: [:login, :list_projects] - before_action :get_gcp_projects, only: [:new, :create] - before_action :verify_billing, only: [:create] + before_action :get_gcp_projects, only: [:new] def login begin @@ -43,21 +42,6 @@ class Projects::Clusters::GcpController < Projects::ApplicationController private - def verify_billing - case gcp_projects&.empty? - when nil - flash.now[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') - when true - flash.now[:alert] = _('Please enable billing for one of your projects to be able to create a Kubernetes cluster, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" } - when false - return - end - - @cluster = ::Clusters::Cluster.new(create_params) - - render :new - end - def create_params params.require(:cluster).permit( :enabled, diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index 8b72c1a8cd9..b57366d3ac8 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -140,32 +140,17 @@ describe Projects::Clusters::GcpController do allow_any_instance_of(described_class).to receive(:get_gcp_projects) end - context 'when google project billing is enabled' do - before do - allow_any_instance_of(described_class).to receive(:gcp_projects).and_return([double]) - 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 } - expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) - expect(project.clusters.first).to be_gcp - expect(project.clusters.first).to be_kubernetes - end + before do + allow_any_instance_of(described_class).to receive(:gcp_projects).and_return([double]) end - context 'when google project billing is not enabled' do - before do - allow_any_instance_of(described_class).to receive(:gcp_projects).and_return([]) - end - - it 'renders the cluster form with an error' do - go - - expect(response).to set_flash.now[:alert] - expect(response).to render_template('new') - 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 } + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + expect(project.clusters.first).to be_gcp + expect(project.clusters.first).to be_kubernetes end end diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 3937fed490c..c8b418d39bc 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -20,154 +20,108 @@ feature 'Gcp Cluster', :js do .to receive(:token_in_session).and_return('token') allow_any_instance_of(Projects::Clusters::GcpController) .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:get_gcp_projects) end - context 'when user has a GCP project with billing enabled' do + context 'when user does not have a cluster and visits cluster index page' do before do - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:get_gcp_projects) - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:gcp_projects).and_return([double]) + visit project_clusters_path(project) + + click_link 'Add Kubernetes cluster' + click_link 'Create on Google Kubernetes Engine' end - context 'when user does not have a cluster and visits cluster index page' do + context 'when user filled form with valid parameters' do before do - visit project_clusters_path(project) - - click_link 'Add Kubernetes cluster' - click_link 'Create on Google Kubernetes Engine' - end - - context 'when user filled form with valid parameters' do - before do - 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) - - fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' - fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Create Kubernetes cluster' + 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 - it 'user sees a cluster details page and creation status' do - expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') + allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil) - Clusters::Cluster.last.provider.make_created! - - expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine') - end - - it 'user sees a error if something worng during creation' do - expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') - - Clusters::Cluster.last.provider.make_errored!('Something wrong!') - - expect(page).to have_content('Something wrong!') - end + fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' + fill_in 'cluster_name', with: 'dev-cluster' + click_button 'Create Kubernetes cluster' end - context 'when user filled form with invalid parameters' do - before do - click_button 'Create Kubernetes cluster' - end + it 'user sees a cluster details page and creation status' do + expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') - it 'user sees a validation error' do - expect(page).to have_css('#error_explanation') - end - end - end - - context 'when user does have a cluster and visits cluster page' do - let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + Clusters::Cluster.last.provider.make_created! - before do - visit project_cluster_path(project, cluster) + expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine') end - it 'user sees a cluster details page' do - expect(page).to have_button('Save changes') - expect(page.find(:css, '.cluster-name').value).to eq(cluster.name) - end + it 'user sees a error if something worng during creation' do + expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') - context 'when user disables the cluster' do - before do - page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click - page.within('#cluster-integration') { click_button 'Save changes' } - end + Clusters::Cluster.last.provider.make_errored!('Something wrong!') - it 'user sees the successful message' do - expect(page).to have_content('Kubernetes cluster was successfully updated.') - end + expect(page).to have_content('Something wrong!') end + end - context 'when user changes cluster parameters' do - before do - fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' - page.within('#js-cluster-details') { click_button 'Save changes' } - end - - it 'user sees the successful message' do - expect(page).to have_content('Kubernetes cluster was successfully updated.') - expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace') - end + context 'when user filled form with invalid parameters' do + before do + click_button 'Create Kubernetes cluster' end - context 'when user destroy the cluster' do - before do - page.accept_confirm do - click_link 'Remove integration' - end - end - - it 'user sees creation form with the successful message' do - expect(page).to have_content('Kubernetes cluster integration was successfully removed.') - expect(page).to have_link('Add Kubernetes cluster') - end + it 'user sees a validation error' do + expect(page).to have_css('#error_explanation') end end end - context 'when user does not have a GCP project with billing enabled' do - before do - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:get_gcp_projects) - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:gcp_projects).and_return([]) - - visit project_clusters_path(project) - - click_link 'Add Kubernetes cluster' - click_link 'Create on Google Kubernetes Engine' + context 'when user does have a cluster and visits cluster page' do + let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } - fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' - fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Create Kubernetes cluster' + before do + visit project_cluster_path(project, cluster) end - it 'user sees form with error' do - expect(page).to have_content('Please enable billing for one of your projects to be able to create a Kubernetes cluster, then try again.') + it 'user sees a cluster details page' do + expect(page).to have_button('Save changes') + expect(page.find(:css, '.cluster-name').value).to eq(cluster.name) end - end - context 'when gcp billing status is not in redis' do - before do - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:get_gcp_projects) - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:gcp_projects).and_return(nil) + context 'when user disables the cluster' do + before do + page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click + page.within('#cluster-integration') { click_button 'Save changes' } + end - visit project_clusters_path(project) + it 'user sees the successful message' do + expect(page).to have_content('Kubernetes cluster was successfully updated.') + end + end - click_link 'Add Kubernetes cluster' - click_link 'Create on Google Kubernetes Engine' + context 'when user changes cluster parameters' do + before do + fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' + page.within('#js-cluster-details') { click_button 'Save changes' } + end - fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' - fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Create Kubernetes cluster' + it 'user sees the successful message' do + expect(page).to have_content('Kubernetes cluster was successfully updated.') + expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace') + end end - it 'user sees form with error' do - expect(page).to have_content('We could not verify that one of your projects on GCP has billing enabled. Please try again.') + context 'when user destroy the cluster' do + before do + page.accept_confirm do + click_link 'Remove integration' + end + end + + it 'user sees creation form with the successful message' do + expect(page).to have_content('Kubernetes cluster integration was successfully removed.') + expect(page).to have_link('Add Kubernetes cluster') + end end end end -- cgit v1.2.1 From e9f31c9ed824e45059c4b8002e259f98c2a6dec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Fri, 20 Apr 2018 23:44:00 +0200 Subject: Remove double before block from GcpController spec --- spec/controllers/projects/clusters/gcp_controller_spec.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index b57366d3ac8..095d1093a22 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -138,9 +138,6 @@ describe Projects::Clusters::GcpController do before do stub_google_api_validate_token allow_any_instance_of(described_class).to receive(:get_gcp_projects) - end - - before do allow_any_instance_of(described_class).to receive(:gcp_projects).and_return([double]) end -- cgit v1.2.1 From 444cee56cb63bb22be27ff783ec501f24523fda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Mon, 23 Apr 2018 17:08:44 +0200 Subject: Use ListGcpProjectsWorker#read_projects instead of get_projects --- app/controllers/projects/clusters/gcp_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb index 585f30cd7f8..0f041e70958 100644 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -71,7 +71,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController end def gcp_projects - ListGcpProjectsWorker.get_projects(token_in_session) + ListGcpProjectsWorker.read_projects(token_in_session) end def token_in_session -- cgit v1.2.1 From 7a1ce56ffacbc1135fae888e76fe56a92d90a304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 24 Apr 2018 20:45:34 +0200 Subject: Handle nil state in ListGcpProjectsWorker#read_projects --- app/workers/list_gcp_projects_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/list_gcp_projects_worker.rb b/app/workers/list_gcp_projects_worker.rb index 690980a4c4e..3e656424bf2 100644 --- a/app/workers/list_gcp_projects_worker.rb +++ b/app/workers/list_gcp_projects_worker.rb @@ -26,7 +26,7 @@ class ListGcpProjectsWorker def self.read_projects(token) Gitlab::Redis::SharedState.with do |redis| value = redis.get(redis_shared_state_key_for(token)) - JSON.parse(value) + value ? JSON.parse(value) : [] end end -- cgit v1.2.1 From 45f52235d9da063bdb190b211d30c13d8b30c711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Wed, 2 May 2018 17:19:50 +0200 Subject: Remove ListGcpProjectsWorker --- app/workers/list_gcp_projects_worker.rb | 92 -------------------- spec/workers/list_gcp_projects_worker_spec.rb | 116 -------------------------- 2 files changed, 208 deletions(-) delete mode 100644 app/workers/list_gcp_projects_worker.rb delete mode 100644 spec/workers/list_gcp_projects_worker_spec.rb diff --git a/app/workers/list_gcp_projects_worker.rb b/app/workers/list_gcp_projects_worker.rb deleted file mode 100644 index 3e656424bf2..00000000000 --- a/app/workers/list_gcp_projects_worker.rb +++ /dev/null @@ -1,92 +0,0 @@ -require 'securerandom' - -class ListGcpProjectsWorker - include ApplicationWorker - include ClusterQueue - - LEASE_TIMEOUT = 3.seconds.to_i - SESSION_KEY_TIMEOUT = 5.minutes - PROJECT_TIMEOUT = 1.hour - BILLING_CHANGED_LABELS = { state_transition: nil }.freeze - - def self.get_session_token(token_key) - Gitlab::Redis::SharedState.with do |redis| - redis.get(get_redis_session_key(token_key)) - end - end - - def self.store_session_token(token) - generate_token_key.tap do |token_key| - Gitlab::Redis::SharedState.with do |redis| - redis.set(get_redis_session_key(token_key), token, ex: SESSION_KEY_TIMEOUT) - end - end - end - - def self.read_projects(token) - Gitlab::Redis::SharedState.with do |redis| - value = redis.get(redis_shared_state_key_for(token)) - value ? JSON.parse(value) : [] - end - end - - def perform(token_key) - return unless token_key - - token = self.class.get_session_token(token_key) - return unless token - return unless try_obtain_lease_for(token) - - billing_enabled_projects = ListGcpProjectsService.new.execute(token) - update_billing_change_counter(!self.class.read_projects(token).empty?, !billing_enabled_projects.empty?) - self.class.store_projects(token, billing_enabled_projects.to_json) - end - - private - - def self.generate_token_key - SecureRandom.uuid - end - - def self.get_redis_session_key(token_key) - "gitlab:gcp:session:#{token_key}" - end - - def self.redis_shared_state_key_for(token) - "gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:projects" - end - - def self.store_projects(token, value) - Gitlab::Redis::SharedState.with do |redis| - redis.set(redis_shared_state_key_for(token), value, ex: PROJECT_TIMEOUT) - end - end - - def try_obtain_lease_for(token) - Gitlab::ExclusiveLease - .new("list_gcp_projects_worker:#{token.hash}", timeout: LEASE_TIMEOUT) - .try_obtain - end - - def billing_changed_counter - @billing_changed_counter ||= Gitlab::Metrics.counter( - :gcp_billing_change_count, - "Counts the number of times a GCP project changed billing_enabled state from false to true", - BILLING_CHANGED_LABELS - ) - end - - def state_transition(previous_state, current_state) - if previous_state.nil? && !current_state - 'no_billing' - elsif previous_state.nil? && current_state - 'with_billing' - elsif !previous_state && current_state - 'billing_configured' - end - end - - def update_billing_change_counter(previous_state, current_state) - billing_changed_counter.increment(state_transition: state_transition(previous_state, current_state)) - end -end diff --git a/spec/workers/list_gcp_projects_worker_spec.rb b/spec/workers/list_gcp_projects_worker_spec.rb deleted file mode 100644 index c415caf78f0..00000000000 --- a/spec/workers/list_gcp_projects_worker_spec.rb +++ /dev/null @@ -1,116 +0,0 @@ -require 'spec_helper' - -describe ListGcpProjectsWorker do - describe '.perform' do - let(:token) { 'bogustoken' } - - subject { described_class.new.perform('token_key') } - - before do - allow(described_class).to receive(:read_projects).and_return([double]) - allow_any_instance_of(described_class).to receive(:update_billing_change_counter) - end - - context 'when there is a token in redis' do - before do - allow(described_class).to receive(:get_session_token).and_return(token) - end - - context 'when there is no lease' do - before do - allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid') - end - - it 'calls the service' do - expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) - - subject - end - - it 'stores projects in redis' do - expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) - expect(described_class).to receive(:store_projects).with(token, anything) - - subject - end - end - - context 'when there is a lease' do - before do - allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return(false) - end - - it 'does not call the service' do - expect(ListGcpProjectsService).not_to receive(:new) - - subject - end - end - end - - context 'when there is no token in redis' do - before do - allow(described_class).to receive(:get_session_token).and_return(nil) - end - - it 'does not call the service' do - expect(ListGcpProjectsService).not_to receive(:new) - - subject - end - end - end - - describe 'billing change counter' do - subject { described_class.new.perform('token_key') } - - before do - allow(described_class).to receive(:get_session_token).and_return('bogustoken') - allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid') - allow(described_class).to receive(:store_projects) - end - - context 'when there are no projects in redis' do - before do - expect(described_class).to receive(:read_projects).and_return([]) - end - - context 'when the service does not return projects' do - before do - expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([]) - end - - it 'increments the billing change counter' do - expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) - - subject - end - end - - context 'when the service returns projects' do - before do - expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) - end - - it 'increments the billing change counter' do - expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) - - subject - end - end - end - - context 'when there are projects in redis' do - before do - expect(described_class).to receive(:read_projects).and_return([double]) - expect(ListGcpProjectsService).to receive_message_chain(:new, :execute).and_return([double]) - end - - it 'increments the billing change counter' do - expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) - - subject - end - end - end -end -- cgit v1.2.1 From 1e8dbd52f09c792a724c0a0e91427f966a03c550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Wed, 2 May 2018 17:20:19 +0200 Subject: Remove ListGcpProjectsService --- app/services/list_gcp_projects_service.rb | 12 ---------- spec/services/list_gcp_projects_service_spec.rb | 32 ------------------------- 2 files changed, 44 deletions(-) delete mode 100644 app/services/list_gcp_projects_service.rb delete mode 100644 spec/services/list_gcp_projects_service_spec.rb diff --git a/app/services/list_gcp_projects_service.rb b/app/services/list_gcp_projects_service.rb deleted file mode 100644 index 7f7b35fd48a..00000000000 --- a/app/services/list_gcp_projects_service.rb +++ /dev/null @@ -1,12 +0,0 @@ -class ListGcpProjectsService - def execute(token) - client = GoogleApi::CloudPlatform::Client.new(token, nil) - # Lists only projects with billing enabled - client.projects_list.select do |project| - begin - client.projects_get_billing_info(project.project_id).billing_enabled - rescue - end - end - end -end diff --git a/spec/services/list_gcp_projects_service_spec.rb b/spec/services/list_gcp_projects_service_spec.rb deleted file mode 100644 index f1957b155e7..00000000000 --- a/spec/services/list_gcp_projects_service_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe ListGcpProjectsService do - include GoogleApi::CloudPlatformHelpers - - let(:service) { described_class.new } - let(:project_id) { 'test-project-1234' } - - describe '#execute' do - before do - stub_cloud_platform_projects_list(project_id: project_id) - end - - subject { service.execute('bogustoken') } - - context 'google account has a billing enabled gcp project' do - before do - stub_cloud_platform_projects_get_billing_info(project_id, true) - end - - it { is_expected.to all(satisfy { |project| project.project_id == project_id }) } - end - - context 'google account does not have a billing enabled gcp project' do - before do - stub_cloud_platform_projects_get_billing_info(project_id, false) - end - - it { is_expected.to eq([]) } - end - end -end -- cgit v1.2.1 From 6472025ea3762667da308d046fca84b14426a644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Wed, 2 May 2018 17:24:55 +0200 Subject: Remove Projects::Clusters::GcpController#list_projects --- .../projects/clusters/gcp_controller.rb | 16 ------------- config/routes/project.rb | 1 - .../projects/clusters/gcp_controller_spec.rb | 28 ---------------------- 3 files changed, 45 deletions(-) diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb index 0f041e70958..6b1a483e28d 100644 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -2,7 +2,6 @@ class Projects::Clusters::GcpController < Projects::ApplicationController before_action :authorize_read_cluster! before_action :authorize_create_cluster!, only: [:new, :create] before_action :authorize_google_api, except: [:login, :list_projects] - before_action :get_gcp_projects, only: [:new] def login begin @@ -34,12 +33,6 @@ class Projects::Clusters::GcpController < Projects::ApplicationController end end - def list_projects - respond_to do |format| - format.json { render status: :ok, json: { projects: gcp_projects } } - end - end - private def create_params @@ -65,15 +58,6 @@ class Projects::Clusters::GcpController < Projects::ApplicationController end end - def get_gcp_projects - redis_token_key = ListGcpProjectsWorker.store_session_token(token_in_session) - ListGcpProjectsWorker.perform_async(redis_token_key) - end - - def gcp_projects - ListGcpProjectsWorker.read_projects(token_in_session) - end - def token_in_session @token_in_session ||= session[GoogleApi::CloudPlatform::Client.session_key_for_token] diff --git a/config/routes/project.rb b/config/routes/project.rb index 8d77863c58b..2a1bcb8cde2 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -206,7 +206,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get '/gcp/new', to: 'clusters/gcp#new' get '/gcp/login', to: 'clusters/gcp#login' - get '/gcp/list_projects', to: 'clusters/gcp#list_projects' post '/gcp', to: 'clusters/gcp#create' end end diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index 095d1093a22..271ba37aed4 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -77,8 +77,6 @@ describe Projects::Clusters::GcpController do end it 'has new object' do - expect(controller).to receive(:get_gcp_projects) - go expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster) @@ -137,8 +135,6 @@ describe Projects::Clusters::GcpController do context 'when access token is valid' do before do stub_google_api_validate_token - allow_any_instance_of(described_class).to receive(:get_gcp_projects) - allow_any_instance_of(described_class).to receive(:gcp_projects).and_return([double]) end it 'creates a new cluster' do @@ -183,28 +179,4 @@ describe Projects::Clusters::GcpController do post :create, params.merge(namespace_id: project.namespace, project_id: project) end end - - describe 'GET list_projects' do - describe 'functionality' do - let(:user) { create(:user) } - let(:gcp_projects) { [project_id: 'test-project-1234'] } - - before do - project.add_master(user) - sign_in(user) - - allow_any_instance_of(described_class).to receive(:gcp_projects).and_return(gcp_projects) - end - - it 'renders the response as json' do - go - - expect(response.body).to eq({ projects: gcp_projects }.to_json) - end - end - - def go - get :list_projects, namespace_id: project.namespace, project_id: project, format: :json - end - end end -- cgit v1.2.1 From ae6a37d70d885e75d8ef5967c4267092af37e893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Wed, 2 May 2018 17:25:33 +0200 Subject: Remove list_gcp_projects queue --- app/workers/all_queues.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 1edfaa1691a..cf05deaef9c 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -24,7 +24,6 @@ - gcp_cluster:cluster_provision - gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:wait_for_cluster_creation -- gcp_cluster:list_gcp_projects - gcp_cluster:cluster_wait_for_ingress_ip_address - github_import_advance_stage -- cgit v1.2.1 From 534bb0b617b6a5b8ca97176e6c7c874056c4c67f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Wed, 2 May 2018 17:28:21 +0200 Subject: Remove list_projects action from Google API check --- app/controllers/projects/clusters/gcp_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb index 6b1a483e28d..6a017c2010b 100644 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -1,7 +1,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController before_action :authorize_read_cluster! before_action :authorize_create_cluster!, only: [:new, :create] - before_action :authorize_google_api, except: [:login, :list_projects] + before_action :authorize_google_api, except: :login def login begin -- cgit v1.2.1 From 3c81b62d9af9b135e28b0567420e37273a6e3176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Wed, 2 May 2018 17:32:57 +0200 Subject: Remove GCP Project list stub from GCP feature spec --- spec/features/projects/clusters/gcp_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index c8b418d39bc..dbf8a22b5ac 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -20,7 +20,6 @@ feature 'Gcp Cluster', :js do .to receive(:token_in_session).and_return('token') allow_any_instance_of(Projects::Clusters::GcpController) .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:get_gcp_projects) end context 'when user does not have a cluster and visits cluster index page' do -- cgit v1.2.1 From a5f43b3d0a6ece692123e3521e8e159dd77bb346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Wed, 2 May 2018 20:44:17 +0200 Subject: Remove project billing context from controller spec --- .../projects/clusters/gcp_controller_spec.rb | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index 10ca2af1995..271ba37aed4 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -137,21 +137,13 @@ describe Projects::Clusters::GcpController do stub_google_api_validate_token end - context 'when google project billing is enabled' do - before do - redis_double = double.as_null_object - allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double) - allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true') - 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 } - expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) - expect(project.clusters.first).to be_gcp - expect(project.clusters.first).to be_kubernetes - 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 } + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + expect(project.clusters.first).to be_gcp + expect(project.clusters.first).to be_kubernetes end end -- cgit v1.2.1 From 7db2ef366566f1105f0dd936f8f7090e1e93b0b2 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Wed, 2 May 2018 16:18:32 +0200 Subject: fetch gke parameters from frontend --- .../pages/projects/clusters/gcp/new/index.js | 117 +++++++++++++ .../components/dropdown_button.vue | 51 ++++++ .../components/gke_machine_type_dropdown.vue | 177 +++++++++++++++++++ .../components/gke_project_id_dropdown.vue | 190 +++++++++++++++++++++ .../components/gke_zone_dropdown.vue | 163 ++++++++++++++++++ .../projects/gke_cluster_dropdowns/eventhub.js | 3 + .../gke_cluster_dropdowns/stores/actions.js | 20 +++ .../gke_cluster_dropdowns/stores/getters.js | 0 .../projects/gke_cluster_dropdowns/stores/index.js | 18 ++ .../gke_cluster_dropdowns/stores/mutation_types.js | 3 + .../gke_cluster_dropdowns/stores/mutations.js | 13 ++ .../components/dropdown/dropdown_hidden_input.vue | 22 +++ .../components/dropdown/dropdown_search_input.vue | 44 +++++ app/assets/stylesheets/pages/clusters.scss | 13 ++ app/views/projects/clusters/gcp/_form.html.haml | 17 +- 15 files changed, 844 insertions(+), 7 deletions(-) create mode 100644 app/assets/javascripts/pages/projects/clusters/gcp/new/index.js create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/components/dropdown_button.vue create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/eventhub.js create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/stores/actions.js create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/stores/getters.js create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/stores/index.js create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/stores/mutation_types.js create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/stores/mutations.js create mode 100644 app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue create mode 100644 app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js new file mode 100644 index 00000000000..d3f22538068 --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js @@ -0,0 +1,117 @@ +/* global gapi */ +import Vue from 'vue'; +import Flash from '~/flash'; +import { s__ } from '~/locale'; +import GkeProjectIdDropdown from '~/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue'; +import GkeZoneDropdown from '~/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue'; +import GkeMachineTypeDropdown from '~/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue'; + +const GCP_API_ERROR = + 'ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later.'; + +function mountGkeProjectIdDropdown() { + const el = document.getElementById('js-gcp-project-id-dropdown-entry-point'); + const hiddenInput = el.querySelector('input'); + + if (!el) return; + // debugger; + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + GkeProjectIdDropdown, + }, + render: createElement => + createElement('gke-project-id-dropdown', { + props: { + docsUrl: el.dataset.docsurl, + service: gapi.client.cloudresourcemanager, + fieldName: hiddenInput.getAttribute('name'), + fieldId: hiddenInput.getAttribute('id'), + }, + }), + }); +} + +function mountGkeZoneDropdown() { + const el = document.getElementById('js-gcp-zone-dropdown-entry-point'); + const hiddenInput = el.querySelector('input'); + + if (!el) return; + // debugger; + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + GkeZoneDropdown, + }, + render: createElement => + createElement('gke-zone-dropdown', { + props: { + service: gapi.client.compute, + fieldName: hiddenInput.getAttribute('name'), + fieldId: hiddenInput.getAttribute('id'), + }, + }), + }); +} + +function mountGkeMachineTypeDropdown() { + const el = document.getElementById('js-gcp-machine-type-dropdown-entry-point'); + const hiddenInput = el.querySelector('input'); + + if (!el) return; + // debugger; + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + GkeMachineTypeDropdown, + }, + render: createElement => + createElement('gke-machine-type-dropdown', { + props: { + service: gapi.client.compute, + fieldName: hiddenInput.getAttribute('name'), + fieldId: hiddenInput.getAttribute('id'), + }, + }), + }); +} + +function initializeGapiClient() { + const el = document.getElementById('new_cluster'); + + gapi.client.setToken({ access_token: el.dataset.token }); + + gapi.client + .load('https://www.googleapis.com/discovery/v1/apis/cloudresourcemanager/v1/rest') + .then(() => { + mountGkeProjectIdDropdown(); + }) + .catch(() => { + Flash(s__(GCP_API_ERROR)); + }); + + gapi.client + .load('https://www.googleapis.com/discovery/v1/apis/compute/v1/rest') + .then(() => { + mountGkeZoneDropdown(); + mountGkeMachineTypeDropdown(); + }) + .catch(() => { + Flash(s__(GCP_API_ERROR)); + }); +} + +document.addEventListener('DOMContentLoaded', () => { + if (typeof gapi === 'undefined') { + Flash(s__(GCP_API_ERROR)); + return false; + } + + gapi.load('client', initializeGapiClient); +}); diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/dropdown_button.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/dropdown_button.vue new file mode 100644 index 00000000000..48819f8aa4c --- /dev/null +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/dropdown_button.vue @@ -0,0 +1,51 @@ + + + diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue new file mode 100644 index 00000000000..9f05b1ab01f --- /dev/null +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue @@ -0,0 +1,177 @@ + + + diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue new file mode 100644 index 00000000000..c37b49441ac --- /dev/null +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue @@ -0,0 +1,190 @@ + + + diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue new file mode 100644 index 00000000000..9b1dfe00f02 --- /dev/null +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue @@ -0,0 +1,163 @@ + + + diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/eventhub.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/eventhub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/eventhub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/actions.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/actions.js new file mode 100644 index 00000000000..45a8c19e638 --- /dev/null +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/actions.js @@ -0,0 +1,20 @@ +import * as types from './mutation_types'; +import eventHub from '../eventhub'; + +export const setProject = ({ commit }, selectedProject) => { + commit(types.SET_PROJECT, selectedProject); + + eventHub.$emit('projectSelected'); +}; + +export const setZone = ({ commit }, selectedZone) => { + commit(types.SET_ZONE, selectedZone); + + eventHub.$emit('zoneSelected'); +}; + +export const setMachineType = ({ commit }, selectedMachineType) => { + commit(types.SET_MACHINE_TYPE, selectedMachineType); + + eventHub.$emit('machineTypeSelected'); +}; diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/getters.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/getters.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/index.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/index.js new file mode 100644 index 00000000000..76e839efa88 --- /dev/null +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/index.js @@ -0,0 +1,18 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + actions, + getters, + mutations, + state: { + selectedProject: '', + selectedZone: '', + selectedMachineType: '', + }, +}); diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/mutation_types.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/mutation_types.js new file mode 100644 index 00000000000..865dc918f82 --- /dev/null +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/mutation_types.js @@ -0,0 +1,3 @@ +export const SET_PROJECT = 'SET_PROJECT'; +export const SET_ZONE = 'SET_ZONE'; +export const SET_MACHINE_TYPE = 'SET_MACHINE_TYPE'; diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/mutations.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/mutations.js new file mode 100644 index 00000000000..e00645b2005 --- /dev/null +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/stores/mutations.js @@ -0,0 +1,13 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_PROJECT](state, selectedProject) { + Object.assign(state, { selectedProject }); + }, + [types.SET_ZONE](state, selectedZone) { + Object.assign(state, { selectedZone }); + }, + [types.SET_MACHINE_TYPE](state, selectedMachineType) { + Object.assign(state, { selectedMachineType }); + }, +}; diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue new file mode 100644 index 00000000000..7f3e88a9a40 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue @@ -0,0 +1,22 @@ + + + diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue new file mode 100644 index 00000000000..aa46e47e84a --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue @@ -0,0 +1,44 @@ + + + diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 7b8ee026357..2d1fd074e2b 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -26,3 +26,16 @@ margin-right: 0; } } + +#new_cluster { + .dropdown-menu-toggle { + .loading-container { + .fa { + position: relative; + margin-top: 2px; + top: initial; + right: initial; + } + } + } +} diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index 5739a57dcfe..4a175c081c1 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -1,8 +1,10 @@ += javascript_include_tag 'https://apis.google.com/js/api.js' + %p - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page} -= form_for @cluster, html: { class: 'prepend-top-20' }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| += form_for @cluster, html: { class: 'prepend-top-20', data: { token: @token_in_session } }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| = form_errors(@cluster) .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') @@ -14,13 +16,14 @@ = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field| .form-group = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') - = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer') - = provider_gcp_field.text_field :gcp_project_id, class: 'form-control', placeholder: s_('ClusterIntegration|Project ID') + #js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } } + = provider_gcp_field.hidden_field :gcp_project_id, class: 'form-control', placeholder: 'Select project' .form-group = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone') = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') - = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a' + #js-gcp-zone-dropdown-entry-point + = provider_gcp_field.hidden_field :zone, class: 'form-control', placeholder: 'us-central1-a' .form-group = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes') @@ -28,8 +31,8 @@ .form-group = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type') - = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer') - = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4' + #js-gcp-machine-type-dropdown-entry-point + = provider_gcp_field.hidden_field :machine_type, class: 'form-control', placeholder: 'us-central1-a' .form-group - = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'btn btn-success' + = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'btn btn-success', disabled: true -- cgit v1.2.1 From 739029bb0f03ff2bf70d67b3d2a09ddd196143a6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 4 May 2018 12:40:37 +0200 Subject: Fix API to remove deploy key from project instead of deleting it entirely --- .../unreleased/security-dm-delete-deploy-key.yml | 5 +++ lib/api/deploy_keys.rb | 6 ++-- spec/requests/api/deploy_keys_spec.rb | 40 +++++++++++++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/security-dm-delete-deploy-key.yml diff --git a/changelogs/unreleased/security-dm-delete-deploy-key.yml b/changelogs/unreleased/security-dm-delete-deploy-key.yml new file mode 100644 index 00000000000..aa94e7b6072 --- /dev/null +++ b/changelogs/unreleased/security-dm-delete-deploy-key.yml @@ -0,0 +1,5 @@ +--- +title: Fix API to remove deploy key from project instead of deleting it entirely +merge_request: +author: +type: security diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 70d43ac1d79..b7aadc27e71 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -148,10 +148,10 @@ module API requires :key_id, type: Integer, desc: 'The ID of the deploy key' end delete ":id/deploy_keys/:key_id" do - key = user_project.deploy_keys.find(params[:key_id]) - not_found!('Deploy Key') unless key + deploy_key_project = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) + not_found!('Deploy Key') unless deploy_key_project - destroy_conditionally!(key) + destroy_conditionally!(deploy_key_project) end end end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index ae9c0e9c304..32fc704a79b 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -171,7 +171,7 @@ describe API::DeployKeys do deploy_key end - it 'deletes existing key' do + it 'removes existing key from project' do expect do delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) @@ -179,6 +179,44 @@ describe API::DeployKeys do end.to change { project.deploy_keys.count }.by(-1) end + context 'when the deploy key is public' do + it 'does not delete the deploy key' do + expect do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + + expect(response).to have_gitlab_http_status(204) + end.not_to change { DeployKey.count } + end + end + + context 'when the deploy key is not public' do + let!(:deploy_key) { create(:deploy_key, public: false) } + + context 'when the deploy key is only used by this project' do + it 'deletes the deploy key' do + expect do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + + expect(response).to have_gitlab_http_status(204) + end.to change { DeployKey.count }.by(-1) + end + end + + context 'when the deploy key is used by other projects' do + before do + create(:deploy_keys_project, project: project2, deploy_key: deploy_key) + end + + it 'does not delete the deploy key' do + expect do + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + + expect(response).to have_gitlab_http_status(204) + end.not_to change { DeployKey.count } + end + end + end + it 'returns 404 Not Found with invalid ID' do delete api("/projects/#{project.id}/deploy_keys/404", admin) -- cgit v1.2.1 From 11e9339d9cd1931d2481f19cfe327af7d14bae44 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Fri, 4 May 2018 13:51:37 +0200 Subject: fix number of form value bugs --- .../components/gke_machine_type_dropdown.vue | 4 +- .../components/gke_project_id_dropdown.vue | 43 ++++++++-------------- .../components/gke_zone_dropdown.vue | 6 ++- .../projects/gke_cluster_dropdowns/stores/index.js | 5 ++- 4 files changed, 26 insertions(+), 32 deletions(-) diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue index 9f05b1ab01f..ff67cf848d2 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue @@ -85,7 +85,7 @@ export default { fetchItems() { this.isLoading = true; const request = this.service.machineTypes.list({ - project: this.$store.state.selectedProject, + project: this.$store.state.selectedProject.projectId, zone: this.$store.state.selectedZone, }); @@ -143,7 +143,7 @@ export default { > item.name.toLowerCase().indexOf(this.searchQuery) > -1); }, toggleText() { - if (this.$store.state.selectedProject) { - return this.$store.state.selectedProject; + if (this.$store.state.selectedProject.name) { + return this.$store.state.selectedProject.name; + } + + if (this.isLoading) { + return s__('ClusterIntegration|Fetching projects'); } - return this.isLoading - ? s__('ClusterIntegration|Fetching projects') - : s__('ClusterIntegration|Select project'); + return this.items.length + ? s__('ClusterIntegration|Select project') + : s__('ClusterIntegration|No projects found'); }, placeholderText() { return s__('ClusterIntegration|Search projects'); @@ -102,26 +107,10 @@ export default { resp => { this.items = resp.result.projects; - // Cause error - // this.items = data; - - // Single state - // this.items = [ - // { - // create_time: '2018-01-16T15:55:02.992Z', - // lifecycle_state: 'ACTIVE', - // name: 'NaturalInterface', - // item_id: 'naturalinterface-192315', - // item_number: 840816084083, - // }, - // ]; - + this.isLoading = false; if (this.items.length === 1) { - this.isDisabled = true; - this.setProject(this.items[0].name); + this.setProject(this.items[0]); } - - this.isLoading = false; }, resp => { this.isLoading = false; @@ -150,7 +139,7 @@ export default { > {{ result.name }} diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue index 9b1dfe00f02..b8f3f0fd8ca 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue @@ -76,7 +76,9 @@ export default { ...mapActions(['setZone']), fetchItems() { this.isLoading = true; - const request = this.service.zones.list({ project: this.$store.state.selectedProject }); + const request = this.service.zones.list({ + project: this.$store.state.selectedProject.projectId, + }); return request.then( resp => { @@ -129,7 +131,7 @@ export default { > Date: Fri, 4 May 2018 14:43:16 +0200 Subject: fix validation for zone dropdowns --- .../projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue index b8f3f0fd8ca..fdfdfb20046 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue @@ -47,7 +47,7 @@ export default { }, computed: { isDisabled() { - return this.$store.state.selectedProject.length === 0; + return this.$store.state.selectedProject.projectId.length === 0; }, results() { return this.items.filter(item => item.name.toLowerCase().indexOf(this.searchQuery) > -1); -- cgit v1.2.1 From eece2a1c6f735d757fdd11eed04cd661fee125fc Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Sun, 6 May 2018 14:42:47 +0200 Subject: change entrypoints + dummy loading elements --- app/views/projects/clusters/gcp/_form.html.haml | 27 +++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index 4a175c081c1..1f2fb445d78 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -16,14 +16,24 @@ = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field| .form-group = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') - #js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } } - = provider_gcp_field.hidden_field :gcp_project_id, class: 'form-control', placeholder: 'Select project' + .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } } + = provider_gcp_field.hidden_field :gcp_project_id + .dropdown + %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true } + %span.dropdown-toggle-text + = _('Select project') + = icon('chevron-down') .form-group = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone') = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') - #js-gcp-zone-dropdown-entry-point - = provider_gcp_field.hidden_field :zone, class: 'form-control', placeholder: 'us-central1-a' + .js-gcp-zone-dropdown-entry-point + = provider_gcp_field.hidden_field :zone + .dropdown + %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true } + %span.dropdown-toggle-text + = _('Select project to choose zone') + = icon('chevron-down') .form-group = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes') @@ -31,8 +41,13 @@ .form-group = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type') - #js-gcp-machine-type-dropdown-entry-point - = provider_gcp_field.hidden_field :machine_type, class: 'form-control', placeholder: 'us-central1-a' + .js-gcp-machine-type-dropdown-entry-point + = provider_gcp_field.hidden_field :machine_type + .dropdown + %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true } + %span.dropdown-toggle-text + = _('Select project and zone to choose machine type') + = icon('chevron-down') .form-group = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'btn btn-success', disabled: true -- cgit v1.2.1 From e754fafad6b8faf06c8154cdd8170cb4567b6835 Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Sun, 6 May 2018 14:42:51 +0200 Subject: revisions --- .../pages/projects/clusters/gcp/new/index.js | 116 +-------------------- .../components/dropdown_button.vue | 1 - .../projects/gke_cluster_dropdowns/constants.js | 9 ++ .../projects/gke_cluster_dropdowns/index.js | 110 +++++++++++++++++++ 4 files changed, 121 insertions(+), 115 deletions(-) create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js create mode 100644 app/assets/javascripts/projects/gke_cluster_dropdowns/index.js diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js index d3f22538068..d4f34e32a48 100644 --- a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js +++ b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js @@ -1,117 +1,5 @@ -/* global gapi */ -import Vue from 'vue'; -import Flash from '~/flash'; -import { s__ } from '~/locale'; -import GkeProjectIdDropdown from '~/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue'; -import GkeZoneDropdown from '~/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue'; -import GkeMachineTypeDropdown from '~/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue'; - -const GCP_API_ERROR = - 'ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later.'; - -function mountGkeProjectIdDropdown() { - const el = document.getElementById('js-gcp-project-id-dropdown-entry-point'); - const hiddenInput = el.querySelector('input'); - - if (!el) return; - // debugger; - - // eslint-disable-next-line no-new - new Vue({ - el, - components: { - GkeProjectIdDropdown, - }, - render: createElement => - createElement('gke-project-id-dropdown', { - props: { - docsUrl: el.dataset.docsurl, - service: gapi.client.cloudresourcemanager, - fieldName: hiddenInput.getAttribute('name'), - fieldId: hiddenInput.getAttribute('id'), - }, - }), - }); -} - -function mountGkeZoneDropdown() { - const el = document.getElementById('js-gcp-zone-dropdown-entry-point'); - const hiddenInput = el.querySelector('input'); - - if (!el) return; - // debugger; - - // eslint-disable-next-line no-new - new Vue({ - el, - components: { - GkeZoneDropdown, - }, - render: createElement => - createElement('gke-zone-dropdown', { - props: { - service: gapi.client.compute, - fieldName: hiddenInput.getAttribute('name'), - fieldId: hiddenInput.getAttribute('id'), - }, - }), - }); -} - -function mountGkeMachineTypeDropdown() { - const el = document.getElementById('js-gcp-machine-type-dropdown-entry-point'); - const hiddenInput = el.querySelector('input'); - - if (!el) return; - // debugger; - - // eslint-disable-next-line no-new - new Vue({ - el, - components: { - GkeMachineTypeDropdown, - }, - render: createElement => - createElement('gke-machine-type-dropdown', { - props: { - service: gapi.client.compute, - fieldName: hiddenInput.getAttribute('name'), - fieldId: hiddenInput.getAttribute('id'), - }, - }), - }); -} - -function initializeGapiClient() { - const el = document.getElementById('new_cluster'); - - gapi.client.setToken({ access_token: el.dataset.token }); - - gapi.client - .load('https://www.googleapis.com/discovery/v1/apis/cloudresourcemanager/v1/rest') - .then(() => { - mountGkeProjectIdDropdown(); - }) - .catch(() => { - Flash(s__(GCP_API_ERROR)); - }); - - gapi.client - .load('https://www.googleapis.com/discovery/v1/apis/compute/v1/rest') - .then(() => { - mountGkeZoneDropdown(); - mountGkeMachineTypeDropdown(); - }) - .catch(() => { - Flash(s__(GCP_API_ERROR)); - }); -} +import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; document.addEventListener('DOMContentLoaded', () => { - if (typeof gapi === 'undefined') { - Flash(s__(GCP_API_ERROR)); - return false; - } - - gapi.load('client', initializeGapiClient); + initGkeDropdowns(); }); diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/dropdown_button.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/dropdown_button.vue index 48819f8aa4c..ff0aba89d20 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/dropdown_button.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/dropdown_button.vue @@ -23,7 +23,6 @@ export default { default: __('Select'), }, }, - computed: {}, }; diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js new file mode 100644 index 00000000000..100331df819 --- /dev/null +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js @@ -0,0 +1,9 @@ +import { s__ } from '~/locale'; + +export const GCP_API_ERROR = s__( + 'ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later.', +); +export const GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT = + 'https://www.googleapis.com/discovery/v1/apis/cloudresourcemanager/v1/rest'; +export const GCP_API_COMPUTE_ENDPOINT = + 'https://www.googleapis.com/discovery/v1/apis/compute/v1/rest'; diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js new file mode 100644 index 00000000000..0ff705b0879 --- /dev/null +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js @@ -0,0 +1,110 @@ +/* global gapi */ +import Vue from 'vue'; +import Flash from '~/flash'; +import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue'; +import GkeZoneDropdown from './components/gke_zone_dropdown.vue'; +import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue'; +import * as CONSTANTS from './constants'; + +const mountGkeProjectIdDropdown = () => { + const el = document.querySelector('.js-gcp-project-id-dropdown-entry-point'); + const hiddenInput = el.querySelector('input'); + + if (!el) return false; + + return new Vue({ + el, + components: { + GkeProjectIdDropdown, + }, + render: createElement => + createElement('gke-project-id-dropdown', { + props: { + docsUrl: el.dataset.docsurl, + service: gapi.client.cloudresourcemanager, + fieldName: hiddenInput.getAttribute('name'), + fieldId: hiddenInput.getAttribute('id'), + }, + }), + }); +}; + +const mountGkeZoneDropdown = () => { + const el = document.querySelector('.js-gcp-zone-dropdown-entry-point'); + const hiddenInput = el.querySelector('input'); + + if (!el) return false; + + return new Vue({ + el, + components: { + GkeZoneDropdown, + }, + render: createElement => + createElement('gke-zone-dropdown', { + props: { + service: gapi.client.compute, + fieldName: hiddenInput.getAttribute('name'), + fieldId: hiddenInput.getAttribute('id'), + }, + }), + }); +}; + +const mountGkeMachineTypeDropdown = () => { + const el = document.querySelector('.js-gcp-machine-type-dropdown-entry-point'); + const hiddenInput = el.querySelector('input'); + + if (!el) return false; + + return new Vue({ + el, + components: { + GkeMachineTypeDropdown, + }, + render: createElement => + createElement('gke-machine-type-dropdown', { + props: { + service: gapi.client.compute, + fieldName: hiddenInput.getAttribute('name'), + fieldId: hiddenInput.getAttribute('id'), + }, + }), + }); +}; + +const gkeDropdownErrorHandler = () => { + Flash(CONSTANTS.GCP_API_ERROR); +}; + +const initializeGapiClient = () => { + const el = document.getElementById('new_cluster'); + + gapi.client.setToken({ access_token: el.dataset.token }); + + gapi.client + .load(CONSTANTS.GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT) + .then(() => { + mountGkeProjectIdDropdown(); + }) + .catch(gkeDropdownErrorHandler); + + gapi.client + .load(CONSTANTS.GCP_API_COMPUTE_ENDPOINT) + .then(() => { + mountGkeZoneDropdown(); + mountGkeMachineTypeDropdown(); + }) + .catch(gkeDropdownErrorHandler); +}; + +const initGkeDropdowns = () => { + if (typeof gapi === 'undefined') { + gkeDropdownErrorHandler(); + return false; + } + + return gapi.load('client', initializeGapiClient); +}; + +export default initGkeDropdowns; -- cgit v1.2.1 From 28a45b02180370cce30aa2e4fe05babc59e3e23b Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Sun, 6 May 2018 14:49:05 +0200 Subject: reduce UI jumping with fake help-block element --- app/views/projects/clusters/gcp/_form.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index 1f2fb445d78..9544089fbde 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -23,6 +23,7 @@ %span.dropdown-toggle-text = _('Select project') = icon('chevron-down') + %span.help-block   .form-group = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone') -- cgit v1.2.1 From 2ea263ea437b98ffb4d61cf9ceb63473c9e6212f Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Sun, 6 May 2018 17:40:48 +0200 Subject: refactoring! --- .../components/gke_machine_type_dropdown.vue | 72 ++++++++++------------ .../components/gke_project_id_dropdown.vue | 52 ++++++++++------ .../components/gke_zone_dropdown.vue | 63 +++++++++---------- .../projects/gke_cluster_dropdowns/index.js | 6 +- .../gke_cluster_dropdowns/stores/getters.js | 3 + app/assets/stylesheets/pages/clusters.scss | 2 +- app/views/projects/clusters/gcp/_form.html.haml | 4 +- 7 files changed, 106 insertions(+), 96 deletions(-) diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue index ff67cf848d2..b55e7bb2d5d 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue @@ -1,7 +1,8 @@ -- cgit v1.2.1 From cf65abd8408da8a3c25f711cb5eac34bbc0adb2e Mon Sep 17 00:00:00 2001 From: Dennis Tang Date: Tue, 8 May 2018 23:42:58 +0200 Subject: no search results state --- .../gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue | 6 ++++++ .../gke_cluster_dropdowns/components/gke_project_id_dropdown.vue | 6 ++++++ .../projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue index 42183a04484..1b9bc5d2cff 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue @@ -73,6 +73,9 @@ export default { searchPlaceholderText() { return s__('ClusterIntegration|Search machine types'); }, + noSearchResultsText() { + return s__('ClusterIntegration|No machine types matched your search'); + }, }, created() { eventHub.$on('zoneSelected', this.fetchMachineTypes); @@ -135,6 +138,9 @@ export default { />