Commit e3d80a79 authored by Thong Kuah's avatar Thong Kuah
Browse files

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
parent 0232c657fd39
......@@ -87,6 +87,12 @@ class Cluster < ActiveRecord::Base
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 @@ def kubeclient
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?
......
......@@ -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
......
# 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
......@@ -96,6 +96,8 @@ def after_create_actions
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 @@ def create_readme
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
......
......@@ -54,6 +54,7 @@ def transfer(project)
end
attempt_transfer_transaction
configure_group_clusters_for_project
end
# rubocop: enable CodeReuse/ActiveRecord
......@@ -162,5 +163,9 @@ def move_project_uploads(project)
@new_namespace.full_path
)
end
def configure_group_clusters_for_project
ClusterProjectConfigureWorker.perform_async(project.id)
end
end
end
......@@ -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
......
......@@ -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
# 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
......@@ -92,6 +92,26 @@
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? }
......
......@@ -155,6 +155,24 @@
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) }
......
# 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
......@@ -261,6 +261,32 @@ def wiki_repo(project)
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)
......
......@@ -62,6 +62,32 @@
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
......
......@@ -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 @@
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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment