diff options
author | Thong Kuah <tkuah@gitlab.com> | 2018-11-15 12:17:41 +0300 |
---|---|---|
committer | Thong Kuah <tkuah@gitlab.com> | 2018-12-05 00:16:44 +0300 |
commit | d54791e0942ae774876db22675cde1b54f35109d (patch) | |
tree | 0921f14ff534d058b321d008c2ff570d043d4cfc | |
parent | 8419b7dd2b85fbe9216a31ce84d5ecb234a8b90a (diff) |
Create k8s namespace for project in group clusters
AFAIK the only relevant place is Projects::CreateService, this gets
called when user creates a new project, forks a new project and does
those things via the api.
Also create k8s namespace for new group hierarchy
when transferring project between groups
Uses new Refresh service to create k8s namespaces
- Ensure we use Cluster#cluster_project
If a project has multiple clusters (EE), using Project#cluster_project
is not guaranteed to return the cluster_project for this cluster. So
switch to using Cluster#cluster_project - at this stage a cluster can
only have 1 cluster_project.
Also, remove rescue so that sidekiq can retry
-rw-r--r-- | app/models/clusters/cluster.rb | 22 | ||||
-rw-r--r-- | app/models/project.rb | 6 | ||||
-rw-r--r-- | app/services/clusters/refresh_service.rb | 33 | ||||
-rw-r--r-- | app/services/projects/create_service.rb | 6 | ||||
-rw-r--r-- | app/services/projects/transfer_service.rb | 5 | ||||
-rw-r--r-- | app/workers/all_queues.yml | 1 | ||||
-rw-r--r-- | app/workers/cluster_platform_configure_worker.rb | 12 | ||||
-rw-r--r-- | app/workers/cluster_project_configure_worker.rb | 12 | ||||
-rw-r--r-- | spec/models/clusters/cluster_spec.rb | 20 | ||||
-rw-r--r-- | spec/models/project_spec.rb | 18 | ||||
-rw-r--r-- | spec/services/clusters/refresh_service_spec.rb | 107 | ||||
-rw-r--r-- | spec/services/projects/create_service_spec.rb | 26 | ||||
-rw-r--r-- | spec/services/projects/transfer_service_spec.rb | 26 | ||||
-rw-r--r-- | spec/workers/cluster_platform_configure_worker_spec.rb | 52 |
14 files changed, 315 insertions, 31 deletions
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 54314f69c3d..73cae8d3b25 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -87,6 +87,12 @@ module Clusters scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) } + scope :missing_kubernetes_namespace, -> (kubernetes_namespaces) do + subquery = kubernetes_namespaces.select('1').where('clusters_kubernetes_namespaces.cluster_id = clusters.id') + + where('NOT EXISTS (?)', subquery) + end + # Returns an ordered list of group clusters order from clusters of closest # group up to furthest ancestor group def self.ordered_group_clusters_for_project(project_id) @@ -161,11 +167,17 @@ module Clusters platform_kubernetes.kubeclient if kubernetes? end - def find_or_initialize_kubernetes_namespace(cluster_project) - kubernetes_namespaces.find_or_initialize_by( - project: cluster_project.project, - cluster_project: cluster_project - ) + def find_or_initialize_kubernetes_namespace_for_project(project) + if project_type? + kubernetes_namespaces.find_or_initialize_by( + project: project, + cluster_project: cluster_project + ) + else + kubernetes_namespaces.find_or_initialize_by( + project: project + ) + end end def allow_user_defined_namespace? diff --git a/app/models/project.rb b/app/models/project.rb index 8f41629e5e5..a3580961d42 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -383,6 +383,12 @@ class Project < ActiveRecord::Base .where(project_ci_cd_settings: { group_runners_enabled: true }) end + scope :missing_kubernetes_namespace, -> (kubernetes_namespaces) do + subquery = kubernetes_namespaces.select('1').where('clusters_kubernetes_namespaces.project_id = projects.id') + + where('NOT EXISTS (?)', subquery) + end + enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600 diff --git a/app/services/clusters/refresh_service.rb b/app/services/clusters/refresh_service.rb new file mode 100644 index 00000000000..51859a002c0 --- /dev/null +++ b/app/services/clusters/refresh_service.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Clusters + class RefreshService + def create_or_update_namespaces_for_cluster(cluster) + cluster_namespaces = cluster.kubernetes_namespaces + + # Create all namespaces that are missing for each project + cluster.all_projects.missing_kubernetes_namespace(cluster_namespaces).each do |project| + kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace_for_project(project) + + ::Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( + cluster: cluster, + kubernetes_namespace: kubernetes_namespace + ).execute + end + end + + def create_or_update_namespaces_for_project(project) + project_namespaces = project.kubernetes_namespaces + + # Create all namespaces that are missing for each cluster + project.all_clusters.missing_kubernetes_namespace(project_namespaces).each do |cluster| + kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace_for_project(project) + + ::Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( + cluster: cluster, + kubernetes_namespace: kubernetes_namespace + ).execute + end + end + end +end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 9e77a3237e3..d03137b63b2 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -96,6 +96,8 @@ module Projects current_user.invalidate_personal_projects_count create_readme if @initialize_with_readme + + configure_group_clusters_for_project end # Refresh the current user's authorizations inline (so they can access the @@ -121,6 +123,10 @@ module Projects Files::CreateService.new(@project, current_user, commit_attrs).execute end + def configure_group_clusters_for_project + ClusterProjectConfigureWorker.perform_async(@project.id) + end + def skip_wiki? !@project.feature_available?(:wiki, current_user) || @skip_wiki end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 9d40ab166ff..9db3fd9cf17 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -54,6 +54,7 @@ module Projects end attempt_transfer_transaction + configure_group_clusters_for_project end # rubocop: enable CodeReuse/ActiveRecord @@ -162,5 +163,9 @@ module Projects @new_namespace.full_path ) end + + def configure_group_clusters_for_project + ClusterProjectConfigureWorker.perform_async(project.id) + end end end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index c0b410472eb..e51da79c6b5 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -29,6 +29,7 @@ - gcp_cluster:wait_for_cluster_creation - gcp_cluster:cluster_wait_for_ingress_ip_address - gcp_cluster:cluster_platform_configure +- gcp_cluster:cluster_project_configure - github_import_advance_stage - github_importer:github_import_import_diff_note diff --git a/app/workers/cluster_platform_configure_worker.rb b/app/workers/cluster_platform_configure_worker.rb index 8f3689f0166..7f14f8efa2f 100644 --- a/app/workers/cluster_platform_configure_worker.rb +++ b/app/workers/cluster_platform_configure_worker.rb @@ -6,17 +6,7 @@ class ClusterPlatformConfigureWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - next unless cluster.cluster_project - - kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project) - - Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( - cluster: cluster, - kubernetes_namespace: kubernetes_namespace - ).execute + Clusters::RefreshService.new.create_or_update_namespaces_for_cluster(cluster) end - - rescue ::Kubeclient::HttpError => err - Rails.logger.error "Failed to create/update Kubernetes namespace for cluster_id: #{cluster_id} with error: #{err.message}" end end diff --git a/app/workers/cluster_project_configure_worker.rb b/app/workers/cluster_project_configure_worker.rb new file mode 100644 index 00000000000..1271b26e945 --- /dev/null +++ b/app/workers/cluster_project_configure_worker.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class ClusterProjectConfigureWorker + include ApplicationWorker + include ClusterQueue + + def perform(project_id) + project = Project.find(project_id) + + ::Clusters::RefreshService.new.create_or_update_namespaces_for_project(project) + end +end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 244cd125967..229956b56b2 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -92,6 +92,26 @@ describe Clusters::Cluster do it { is_expected.to contain_exactly(cluster) } end + describe '.missing_kubernetes_namespace' do + let!(:cluster) { create(:cluster, :provided_by_gcp, :project) } + let(:project) { cluster.project } + let(:kubernetes_namespaces) { project.kubernetes_namespaces } + + subject do + described_class.joins(:projects).where(projects: { id: project.id }).missing_kubernetes_namespace(kubernetes_namespaces) + end + + it { is_expected.to contain_exactly(cluster) } + + context 'kubernetes namespace exists' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + it { is_expected.to be_empty } + end + end + describe 'validation' do subject { cluster.valid? } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5cce8d87c96..8b1743def72 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -155,6 +155,24 @@ describe Project do it { is_expected.to include_module(Sortable) } end + describe '.missing_kubernetes_namespace' do + let!(:project) { create(:project) } + let!(:cluster) { create(:cluster, :provided_by_user, :group) } + let(:kubernetes_namespaces) { project.kubernetes_namespaces } + + subject { described_class.missing_kubernetes_namespace(kubernetes_namespaces) } + + it { is_expected.to contain_exactly(project) } + + context 'kubernetes namespace exists' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + it { is_expected.to be_empty } + end + end + describe 'validation' do let!(:project) { create(:project) } diff --git a/spec/services/clusters/refresh_service_spec.rb b/spec/services/clusters/refresh_service_spec.rb new file mode 100644 index 00000000000..0bf2bd55c7f --- /dev/null +++ b/spec/services/clusters/refresh_service_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::RefreshService do + shared_examples 'creates a kubernetes namespace' do + let(:token) { 'aaaaaa' } + let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateServiceAccountService, execute: true) } + let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) } + + it 'creates a kubernetes namespace' do + expect(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) + expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) + + expect { subject }.to change(project.kubernetes_namespaces, :count) + + kubernetes_namespace = cluster.kubernetes_namespaces.first + expect(kubernetes_namespace).to be_present + expect(kubernetes_namespace.project).to eq(project) + end + end + + shared_examples 'does not create a kubernetes namespace' do + it 'does not create a new kubernetes namespace' do + expect(Clusters::Gcp::Kubernetes::CreateServiceAccountService).not_to receive(:namespace_creator) + expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).not_to receive(:new) + + expect { subject }.not_to change(Clusters::KubernetesNamespace, :count) + end + end + + describe '#create_or_update_namespaces_for_cluster' do + let(:cluster) { create(:cluster, :provided_by_user, :project) } + let(:project) { cluster.project } + + subject { described_class.new.create_or_update_namespaces_for_cluster(cluster) } + + context 'cluster is project level' do + include_examples 'creates a kubernetes namespace' + + context 'when project already has kubernetes namespace' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + include_examples 'does not create a kubernetes namespace' + end + end + + context 'cluster is group level' do + let(:cluster) { create(:cluster, :provided_by_user, :group) } + let(:group) { cluster.group } + let(:project) { create(:project, group: group) } + + include_examples 'creates a kubernetes namespace' + + context 'when project already has kubernetes namespace' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + include_examples 'does not create a kubernetes namespace' + end + end + end + + describe '#create_or_update_namespaces_for_project' do + let(:project) { create(:project) } + + subject { described_class.new.create_or_update_namespaces_for_project(project) } + + it 'creates no kubernetes namespaces' do + expect { subject }.not_to change(project.kubernetes_namespaces, :count) + end + + context 'project has a project cluster' do + let!(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :project_type, projects: [project]) } + + include_examples 'creates a kubernetes namespace' + + context 'when project already has kubernetes namespace' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + include_examples 'does not create a kubernetes namespace' + end + end + + context 'project belongs to a group cluster' do + let!(:cluster) { create(:cluster, :provided_by_gcp, :group) } + + let(:group) { cluster.group } + let(:project) { create(:project, group: group) } + + include_examples 'creates a kubernetes namespace' + + context 'when project already has kubernetes namespace' do + before do + create(:cluster_kubernetes_namespace, project: project, cluster: cluster) + end + + include_examples 'does not create a kubernetes namespace' + end + end + end +end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 08de27ca44a..07388eb133f 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -261,6 +261,32 @@ describe Projects::CreateService, '#execute' do end end + context 'when group has kubernetes cluster' do + let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) } + let(:group) { group_cluster.group } + + let(:token) { 'aaaa' } + let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateServiceAccountService, execute: true) } + let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) } + + before do + group.add_owner(user) + + expect(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) + expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) + end + + it 'creates kubernetes namespace for the project' do + project = create_project(user, opts.merge!(namespace_id: group.id)) + + expect(project).to be_valid + + kubernetes_namespace = group_cluster.kubernetes_namespaces.first + expect(kubernetes_namespace).to be_present + expect(kubernetes_namespace.project).to eq(project) + end + end + context 'when there is an active service template' do before do create(:service, project: nil, template: true, active: true) diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 2e07d4f8013..5e0f2991a63 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -62,6 +62,32 @@ describe Projects::TransferService do expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" end + + context 'new group has a kubernetes cluster' do + let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) } + let(:group) { group_cluster.group } + + let(:token) { 'aaaa' } + let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateServiceAccountService, execute: true) } + let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) } + + subject { transfer_project(project, user, group) } + + before do + expect(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) + expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) + end + + it 'creates kubernetes namespace for the project' do + subject + + expect(project.kubernetes_namespaces.count).to eq(1) + + kubernetes_namespace = group_cluster.kubernetes_namespaces.first + expect(kubernetes_namespace).to be_present + expect(kubernetes_namespace.project).to eq(project) + end + end end context 'when transfer fails' do diff --git a/spec/workers/cluster_platform_configure_worker_spec.rb b/spec/workers/cluster_platform_configure_worker_spec.rb index b51f6e07c6a..0eead0ab13d 100644 --- a/spec/workers/cluster_platform_configure_worker_spec.rb +++ b/spec/workers/cluster_platform_configure_worker_spec.rb @@ -2,7 +2,43 @@ require 'spec_helper' -describe ClusterPlatformConfigureWorker, '#execute' do +describe ClusterPlatformConfigureWorker, '#perform' do + let(:worker) { described_class.new } + + context 'when group cluster' do + let(:cluster) { create(:cluster, :group, :provided_by_gcp) } + let(:group) { cluster.group } + + context 'when group has no projects' do + it 'does not create a namespace' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:execute) + + worker.perform(cluster.id) + end + end + + context 'when group has a project' do + let!(:project) { create(:project, group: group) } + + it 'creates a namespace for the project' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once + + worker.perform(cluster.id) + end + end + + context 'when group has project in a sub-group' do + let!(:subgroup) { create(:group, parent: group) } + let!(:project) { create(:project, group: subgroup) } + + it 'creates a namespace for the project' do + expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once + + worker.perform(cluster.id) + end + end + end + context 'when provider type is gcp' do let(:cluster) { create(:cluster, :project, :provided_by_gcp) } @@ -30,18 +66,4 @@ describe ClusterPlatformConfigureWorker, '#execute' do described_class.new.perform(123) end end - - context 'when kubeclient raises error' do - let(:cluster) { create(:cluster, :project) } - - it 'rescues and logs the error' do - allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).and_raise(::Kubeclient::HttpError.new(500, 'something baaaad happened', '')) - - expect(Rails.logger) - .to receive(:error) - .with("Failed to create/update Kubernetes namespace for cluster_id: #{cluster.id} with error: something baaaad happened") - - described_class.new.perform(cluster.id) - end - end end |