Skip to content
Snippets Groups Projects
Commit ef5165ac authored by Mayra Cabrera's avatar Mayra Cabrera
Browse files

Adds Cluster::KubernetesNamespace model

- This model will save namespace and service account information (name
  and token)
token)
- Currently we have a 1-1 relationship between Clusters::Project and
  Clusters::KubernetesNamespace
parent e6e33894
No related branches found
No related tags found
No related merge requests found
Showing
with 434 additions and 148 deletions
# frozen_string_literal: true
module Clusters
class KubernetesNamespace < ActiveRecord::Base
self.table_name = 'clusters_kubernetes_namespaces'
belongs_to :cluster_project, class_name: 'Clusters::Project'
has_one :cluster, through: :cluster_project
has_one :project, through: :cluster_project
validates :namespace, presence: true
before_validation :set_cluster_namespace_and_service_account
before_validation :ensure_namespace_uniqueness
attr_encrypted :encrypted_service_account_token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
private
def set_cluster_namespace_and_service_account
self.namespace = build_kubernetes_namespace
self.service_account_name = build_service_account_name
end
def build_kubernetes_namespace
gcp_kubernetes_namespace.presence || default_namespace
end
def build_service_account_name
if cluster.platform_kubernetes_rbac?
"#{default_service_account_name}-#{namespace}"
else
default_service_account_name
end
end
def gcp_kubernetes_namespace
@gcp_kubernetes_namespace ||= cluster&.platform_kubernetes&.namespace
end
def default_namespace
project_slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
def project_slug
"#{project.path}-#{project.id}".downcase
end
def default_service_account_name
Clusters::Gcp::Kubernetes::SERVICE_ACCOUNT_NAME
end
def ensure_namespace_uniqueness
errors.add(:namespace, "Kubernetes namespace #{namespace} already exists on cluster") if kubernetes_namespace_exists?
end
def kubernetes_namespace_exists?
cluster_project.kubernetes_namespaces.where(namespace: namespace).exists?
end
end
end
Loading
Loading
@@ -11,6 +11,8 @@ module Clusters
self.table_name = 'cluster_platforms_kubernetes'
self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] }
 
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
 
attr_encrypted :password,
Loading
Loading
@@ -38,6 +40,7 @@ module Clusters
validates :token, presence: true
 
validate :prevent_modification, on: :update
validate :prevent_reserved_namespaces
 
after_save :clear_reactive_cache!
after_update :update_kubernetes_namespace
Loading
Loading
@@ -59,12 +62,6 @@ module Clusters
}
 
def actual_namespace
cluster_project&.namespace || fallback_actual_namespace
end
# DEPRECATED
# To remove after migration of data to cluster_projects
def fallback_actual_namespace
if namespace.present?
namespace
else
Loading
Loading
@@ -125,8 +122,12 @@ module Clusters
ca_pem: ca_pem)
end
 
# DEPRECATED
def default_namespace
cluster_project&.kubernetes_namespace&.namespace.presence || fallback_default_namespace
end
# DEPRECATED
def fallback_default_namespace
return unless project
 
slug = "#{project.path}-#{project.id}".downcase
Loading
Loading
@@ -199,6 +200,14 @@ module Clusters
true
end
 
def prevent_reserved_namespaces
return if namespace.blank?
if RESERVED_NAMESPACES.include?(namespace)
errors.add(:namespace, 'Cannot used a GitLab reserved namespace')
end
end
def update_kubernetes_namespace
return unless namespace_changed?
 
Loading
Loading
Loading
Loading
@@ -6,20 +6,14 @@ module Clusters
 
belongs_to :cluster, class_name: 'Clusters::Cluster'
belongs_to :project, class_name: '::Project'
has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace', foreign_key: :cluster_project_id
 
attr_encrypted :encrypted_service_account_token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
def last_kubernetes_namespace
return @last_kubernetes_namespace if defined?(@last_kubernetes_namespace)
 
def default_namespace
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
private
def slug
"#{project.path}-#{project.id}".downcase
@first_kubernetes_namespace = kubernetes_namespaces.last
end
alias_method :kubernetes_namespace, :last_kubernetes_namespace
end
end
Loading
Loading
@@ -14,36 +14,21 @@ module Clusters
def execute
return unless platform.cluster_project
 
kubernetes_namespace.ensure_exists!
cluster_kubernetes_namespace.ensure_exists!
 
platform.cluster_project.update!(
namespace: kubernetes_namespace.name,
service_account_name: service_account_name
)
# To do: Create service account
end
 
private
 
def kubernetes_namespace
strong_memoize(:kubernetes_namespace) do
def cluster_kubernetes_namespace
strong_memoize(:cluster_kubernetes_namespace) do
Gitlab::Kubernetes::Namespace.new(namespace_name, platform.kubeclient)
end
end
 
def namespace_name
platform.namespace.presence || platform.cluster_project.default_namespace
end
def service_account_name
if platform.rbac?
"#{default_service_account_name}-#{namespace_name}"
else
default_service_account_name
end
end
def default_service_account_name
Clusters::Gcp::Kubernetes::SERVICE_ACCOUNT_NAME
platform.cluster_project.kubernetes_namespace.namespace
end
end
end
Loading
Loading
# frozen_string_literal: true
class AddKubernetesColumnsToClusterProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column(:cluster_projects, :namespace, :string)
add_column(:cluster_projects, :service_account_name, :string)
add_column(:cluster_projects, :encrypted_service_account_token, :text)
end
end
# frozen_string_literal: true
class CreateClustersKubernetesNamespaces < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table(:clusters_kubernetes_namespaces) do |t|
t.references :cluster_project, null: false, index: true, foreign_key: { on_delete: :cascade }
t.timestamps_with_timezone null: false
t.text :encrypted_service_account_token
t.string :namespace, null: false
t.string :service_account_name
end
end
end
# frozen_string_literal: true
 
class PopulateClusterProjectNamespace < ActiveRecord::Migration
class PopulateClusterKubernetesNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
 
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
 
disable_ddl_transaction!
Loading
Loading
@@ -13,10 +12,16 @@ class PopulateClusterProjectNamespace < ActiveRecord::Migration
self.table_name = 'cluster_projects'
end
 
class ClusterKubernetesNamespace < ActiveRecord::Base
self.table_name = 'clusters_kubernetes_namespaces'
end
def up
ClusterProject.where(namespace: nil).tap do |relation|
cluster_project_with_no_namespace = ClusterProject.where.not(id: ClusterKubernetesNamespace.select(:id))
cluster_project_with_no_namespace.tap do |relation|
queue_background_migration_jobs_by_range_at_intervals(relation,
'PopulateClusterProjectNamespace',
'PopulateClusterKubernetesNamespace',
5.minutes,
batch_size: 500)
end
Loading
Loading
Loading
Loading
@@ -595,9 +595,6 @@ ActiveRecord::Schema.define(version: 20181013005024) do
t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "namespace"
t.string "service_account_name"
t.text "encrypted_service_account_token"
end
 
add_index "cluster_projects", ["cluster_id"], name: "index_cluster_projects_on_cluster_id", using: :btree
Loading
Loading
@@ -694,6 +691,17 @@ ActiveRecord::Schema.define(version: 20181013005024) do
add_index "clusters_applications_runners", ["cluster_id"], name: "index_clusters_applications_runners_on_cluster_id", unique: true, using: :btree
add_index "clusters_applications_runners", ["runner_id"], name: "index_clusters_applications_runners_on_runner_id", using: :btree
 
create_table "clusters_kubernetes_namespaces", force: :cascade do |t|
t.integer "cluster_project_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.text "encrypted_service_account_token"
t.string "namespace", null: false
t.string "service_account_name"
end
add_index "clusters_kubernetes_namespaces", ["cluster_project_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_project_id", using: :btree
create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
Loading
Loading
@@ -2328,6 +2336,7 @@ ActiveRecord::Schema.define(version: 20181013005024) do
add_foreign_key "clusters_applications_prometheus", "clusters", name: "fk_557e773639", on_delete: :cascade
add_foreign_key "clusters_applications_runners", "ci_runners", column: "runner_id", name: "fk_02de2ded36", on_delete: :nullify
add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade
add_foreign_key "clusters_kubernetes_namespaces", "cluster_projects", on_delete: :cascade
add_foreign_key "container_repositories", "projects"
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
Loading
Loading
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class PopulateClusterKubernetesNamespace
module Migratable
class ClusterKubernetesNamespace < ActiveRecord::Base
self.table_name = 'clusters_kubernetes_namespaces'
end
class ClusterProject < ActiveRecord::Base
self.table_name = 'cluster_projects'
belongs_to :project
belongs_to :cluster
def default_namespace
slug = "#{project.path}-#{project.id}".downcase
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
def default_service_account
if cluster.rbac?
"gitlab-#{default_namespace}"
else
"gitlab"
end
end
end
class Project < ActiveRecord::Base
self.table_name = 'projects'
end
class Cluster < ActiveRecord::Base
self.table_name = 'clusters'
has_one :platform_kubernetes
def rbac?
platform_kubernetes.rbac?
end
end
class PlatformKubernetes < ActiveRecord::Base
self.table_name = 'cluster_platforms_kubernetes'
belongs_to :cluster
def rbac?
authorization_type == :rbac
end
end
end
def perform(start_id, stop_id)
cluster_kubernetes_namespace_attributes = []
cluster_project_collection(start_id, stop_id).each do |cluster_project|
attributes = {
cluster_project_id: cluster_project.id,
namespace: cluster_project.default_namespace,
service_account_name: cluster_project.default_service_account
}
cluster_kubernetes_namespace_attributes << attributes
end
Migratable::ClusterKubernetesNamespace.create(cluster_kubernetes_namespace_attributes)
end
private
def cluster_project_collection(start_id, stop_id)
Migratable::ClusterProject.where(id: (start_id..stop_id))
end
end
end
end
Loading
Loading
@@ -396,7 +396,7 @@ describe Projects::ClustersController do
=======
before do
allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
stub_kubeclient_get_namespace('my-namespace')
stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
end
 
context 'when cluster is provided by GCP' do
Loading
Loading
# frozen_string_literal: true
FactoryBot.define do
factory :cluster_kubernetes_namespace, class: Clusters::KubernetesNamespace do
cluster_project
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespace, :migration, schema: 20181009205043 do
let(:migration) { described_class.new }
let(:clusters) { create_list(:cluster, 10, :provided_by_gcp) }
let(:cluster_projects) { Clusters::Project.all }
before do
clusters.each do |cluster|
create(:cluster_project, cluster: cluster)
end
end
subject { migration.perform(cluster_projects.min, cluster_projects.max) }
it 'should populate namespace and service account information' do
subject
cluster_projects.each do |cluster_project|
expect(cluster_project.kubernetes_namespace.namespace).not_to be_nil
expect(cluster_project.kubernetes_namespace.service_account_name).not_to be_nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::BackgroundMigration::PopulateClusterProjectNamespace, :migration, schema: 20181009163424 do
let(:migration) { described_class.new }
let!(:cluster_projects) { create_list(:cluster_project, 10, namespace: nil) }
subject { migration.perform(cluster_projects.min, cluster_projects.max) }
it 'should update cluster project namespaces' do
subject
Clusters::Project.all.each do |cluster_project|
expect(cluster_project.namespace).not_to be_nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::KubernetesNamespace, type: :model do
it { should belong_to(:cluster_project) }
it { should have_one(:project) }
it { should have_one(:cluster) }
describe '#set_cluster_namespace_and_service_account' do
let(:cluster) { platform.cluster }
let(:cluster_project) { create(:cluster_project, cluster: cluster) }
let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, cluster_project: cluster_project) }
describe '#namespace' do
let(:platform) { create(:cluster_platform_kubernetes, namespace: namespace) }
subject { kubernetes_namespace.namespace }
context 'when kubernetes platform has a namespace assigned' do
let(:namespace) { 'my-own-namespace' }
it 'should copy the namespace' do
kubernetes_namespace.save
is_expected.to eq('my-own-namespace')
end
end
context 'when kubernetes platform does not have namespace assigned' do
let(:namespace) { nil }
it 'should set default namespace' do
kubernetes_namespace.save
project_slug = "#{cluster_project.project.path}-#{cluster_project.project_id}"
is_expected.to eq(project_slug)
end
end
end
describe '#service_account_namespace' do
subject { kubernetes_namespace.service_account_name }
context 'when cluster is not using RBAC' do
let(:platform) { create(:cluster_platform_kubernetes) }
it 'should set default service account name' do
kubernetes_namespace.save
is_expected.to eq('gitlab')
end
end
context 'when cluster is using RBAC' do
let(:platform) { create(:cluster_platform_kubernetes, :rbac_enabled) }
it 'should set a service account name based on namespace' do
kubernetes_namespace.save
is_expected.to eq("gitlab-#{kubernetes_namespace.namespace}")
end
end
end
end
describe '#ensure_namespace_uniqueness' do
let(:platform) { create(:cluster_platform_kubernetes) }
let(:cluster) { platform.cluster }
let(:cluster_project) { create(:cluster_project, cluster: cluster) }
let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, cluster_project: cluster_project) }
subject { kubernetes_namespace }
context 'when cluster does not have the kubernetes namespace' do
it { is_expected.to be_valid }
end
context 'when cluster has the same kubernetes namespace' do
before do
create(:cluster_kubernetes_namespace,
namespace: 'my-namespace',
cluster_project: cluster_project)
end
it { is_expected.to_not be_valid }
it 'should return an error on namespace' do
subject.save
project_slug = "#{cluster_project.project.path}-#{cluster_project.project_id}"
expect(subject.errors[:namespace].first).to eq("Kubernetes namespace #{project_slug} already exists on cluster")
end
end
end
end
Loading
Loading
@@ -90,6 +90,29 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { expect(kubernetes.save).to be_falsey }
end
end
describe '#prevent_reserved_namespaces' do
subject { build(:cluster_platform_kubernetes, namespace: namespace) }
context 'when no namespace is manually assigned' do
let(:namespace) { nil }
it { is_expected.to be_valid }
end
context 'when no reserved namespace is assigned' do
let(:namespace) { 'my-namespace' }
it { is_expected.to be_valid }
end
context 'when reserved namespace is assigned' do
let(:namespace) { 'gitlab-managed-apps' }
it { is_expected.to_not be_valid }
end
end
end
 
describe '#kubeclient' do
Loading
Loading
@@ -123,16 +146,6 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
let(:project) { cluster.project }
let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
 
context 'namespace is persisted' do
let(:namespace) { 'namespace-123' }
before do
project.cluster_project.update!(namespace: 'hello-namespace')
end
it { is_expected.to eq('hello-namespace') }
end
context 'when namespace is present' do
let(:namespace) { 'namespace-123' }
 
Loading
Loading
@@ -142,7 +155,19 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
context 'when namespace is not present' do
let(:namespace) { nil }
 
it { is_expected.to eq("#{project.path}-#{project.id}") }
context 'when kubernetes namespace is present' do
let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster_project: kubernetes.cluster_project) }
before do
kubernetes_namespace
end
it { is_expected.to eq(kubernetes_namespace.namespace) }
end
context 'when kubernetes namespace is not present' do
it { is_expected.to eq("#{project.path}-#{project.id}") }
end
end
end
 
Loading
Loading
Loading
Loading
@@ -3,13 +3,43 @@ require 'spec_helper'
describe Clusters::Project do
it { is_expected.to belong_to(:cluster) }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:kubernetes_namespaces) }
 
describe '#default_namespace' do
let(:cluster_project) { build(:cluster_project) }
let(:project) { cluster_project.project }
describe '#kubernetes_namespace' do
let(:cluster) { create(:cluster, :provided_by_gcp) }
let(:cluster_project) { create(:cluster_project, cluster: cluster) }
 
subject { cluster_project.default_namespace }
subject { cluster_project.kubernetes_namespace }
 
it { is_expected.to eq("#{project.path}-#{project.id}") }
before do
kubernetes_namespace
end
context 'when is just one' do
let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster_project: cluster_project) }
it 'returns that one' do
is_expected.to eq(kubernetes_namespace)
end
end
context 'when cluster has many kubernetes namespaces' do
let(:namespaces) { %w(namespace1 namespace2 namespace3) }
let(:kubernetes_namespaces) do
namespaces.each do |namespace|
cluster.platform_kubernetes.update_column(:namespace, namespace)
create(:cluster_kubernetes_namespace, cluster_project: cluster_project)
end
cluster_project.kubernetes_namespaces
end
let(:kubernetes_namespace) { kubernetes_namespaces.last }
it 'returns the last one' do
is_expected.to eq(kubernetes_namespace)
end
end
end
end
Loading
Loading
@@ -3,66 +3,33 @@
require 'spec_helper'
 
describe Clusters::Kubernetes::ConfigureService, '#execute' do
let(:platform) { create(:cluster_platform_kubernetes) }
let(:kubeclient) { platform.kubeclient }
let(:service) { described_class.new(platform) }
subject { service.execute }
shared_examples 'creates a kubernetes namespace' do
before do
platform.cluster.projects << project
allow(kubeclient).to receive(:get_namespace).and_return(nil)
allow(kubeclient).to receive(:create_namespace).and_return(nil)
end
it 'creates a kubernetes namespace' do
expect(kubeclient).to receive(:get_namespace).once.ordered
expect(kubeclient).to receive(:create_namespace).once.ordered
include KubernetesHelpers
 
subject
end
it 'saves namespace and service account into database' do
subject
expect(cluster_project.namespace).to eq(namespace_name)
expect(cluster_project.service_account_name).to eq(service_account_name)
end
end
let(:cluster) { create(:cluster, :provided_by_gcp) }
let(:cluster_project) { create(:cluster_project, cluster: cluster) }
let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster_project: cluster_project) }
let(:kubeclient) { platform.kubeclient }
let(:platform) { kubernetes_namespace.cluster.platform_kubernetes }
let(:namespace) { "#{cluster_project.project.path}-#{cluster_project.project_id}" }
 
context 'no project' do
it { is_expected.to be_nil }
let(:service) do
described_class.new(platform)
end
 
context 'when platform has namespace' do
let(:platform) { create(:cluster_platform_kubernetes, namespace: 'my-namespace') }
let(:project) { create(:project, name: 'hello') }
let(:cluster_project) { project.cluster_project }
it_behaves_like 'creates a kubernetes namespace' do
let(:namespace_name) { 'my-namespace' }
let(:service_account_name) { 'gitlab' }
end
subject { service.execute }
 
context 'when platform is RBAC' do
let(:platform) { create(:cluster_platform_kubernetes, :rbac_enabled, namespace: 'my-namespace') }
before do
api_url = 'https://kubernetes.example.com'
 
it_behaves_like 'creates a kubernetes namespace' do
let(:namespace_name) { 'my-namespace' }
let(:service_account_name) { 'gitlab-my-namespace' }
end
end
stub_kubeclient_discover(api_url)
stub_kubeclient_get_namespace(api_url, namespace: namespace)
stub_kubeclient_create_namespace(api_url)
end
 
context 'when cluster has project related' do
let(:project) { create(:project, name: 'hello') }
let(:cluster_project) { project.cluster_project }
it 'creates a kubernetes namespace' do
expect(kubeclient).to receive(:get_namespace).once.ordered
expect(kubeclient).to receive(:create_namespace).once.ordered
 
it_behaves_like 'creates a kubernetes namespace' do
let(:namespace_name) { "hello-#{project.id}" }
let(:service_account_name) { 'gitlab' }
end
subject
end
end
Loading
Loading
@@ -38,7 +38,7 @@ describe Clusters::UpdateService do
 
before do
allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
stub_kubeclient_get_namespace('custom-namespace')
stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
end
 
it 'updates namespace' do
Loading
Loading
Loading
Loading
@@ -97,7 +97,8 @@ module KubernetesHelpers
{ "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
{ "name" => "secrets", "namespaced" => true, "kind" => "Secret" },
{ "name" => "serviceaccounts", "namespaced" => true, "kind" => "ServiceAccount" },
{ "name" => "services", "namespaced" => true, "kind" => "Service" }
{ "name" => "services", "namespaced" => true, "kind" => "Service" },
{ "name" => "namespaces", "namespaced" => true, "kind" => "Namespace" }
]
}
end
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment