Commit 9f8ba866 authored by Mayra Cabrera's avatar Mayra Cabrera

Add Clusters::KubernetesNamespace model

This model will be used to persist into database Kubernetes properties,
such as namespace, service account name and service account token.
parent f99e2977
......@@ -33,6 +33,9 @@ module Clusters
has_one :application_runner, class_name: 'Clusters::Applications::Runner'
has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter'
has_many :kubernetes_namespaces
has_one :kubernetes_namespace, -> { order(id: :desc) }, class_name: 'Clusters::KubernetesNamespace'
accepts_nested_attributes_for :provider_gcp, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true
......
# frozen_string_literal: true
module Clusters
class KubernetesNamespace < ActiveRecord::Base
self.table_name = 'clusters_kubernetes_namespaces'
belongs_to :cluster_project, class_name: 'Clusters::Project'
belongs_to :cluster, class_name: 'Clusters::Cluster'
belongs_to :project, class_name: '::Project'
has_one :platform_kubernetes, through: :cluster
validates :namespace, presence: true
validates :namespace, uniqueness: { scope: :cluster_id }
before_validation :set_namespace_and_service_account_to_default, on: :create
attr_encrypted :service_account_token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
def token_name
"#{namespace}-token"
end
private
def set_namespace_and_service_account_to_default
self.namespace ||= default_namespace
self.service_account_name ||= default_service_account_name
end
def default_namespace
platform_kubernetes&.namespace.presence || project_namespace
end
def default_service_account_name
"#{namespace}-service-account"
end
def project_namespace
Gitlab::NamespaceSanitizer.sanitize(project_slug)
end
def project_slug
"#{project.path}-#{project.id}".downcase
end
end
end
......@@ -9,6 +9,8 @@ module Clusters
prepend EE::KubernetesService
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
self.table_name = 'cluster_platforms_kubernetes'
self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] }
......@@ -34,6 +36,8 @@ module Clusters
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
validates :namespace, exclusion: { in: RESERVED_NAMESPACES }
# We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
validates :api_url, url: true, presence: true
validates :token, presence: true
......@@ -47,6 +51,7 @@ module Clusters
delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, to: :cluster, allow_nil: true
delegate :managed?, to: :cluster, allow_nil: true
delegate :kubernetes_namespace, to: :cluster
alias_method :active?, :enabled?
......@@ -118,10 +123,19 @@ module Clusters
end
def default_namespace
kubernetes_namespace&.namespace.presence || fallback_default_namespace
end
# DEPRECATED
#
# On 11.4 Clusters::KubernetesNamespace was introduced, this model will allow to
# have multiple namespaces per project. This method will be removed after migration
# has been completed.
def fallback_default_namespace
return unless project
slug = "#{project.path}-#{project.id}".downcase
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
Gitlab::NamespaceSanitizer.sanitize(slug)
end
def build_kube_client!(api_groups: ['api'], api_version: 'v1')
......
......@@ -6,5 +6,8 @@ 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
has_one :kubernetes_namespace, -> { order(id: :desc) }, class_name: 'Clusters::KubernetesNamespace', foreign_key: :cluster_project_id
end
end
---
title: Introduce new model to persist specific cluster information
merge_request: 22404
author:
type: added
# frozen_string_literal: true
class CreateClustersKubernetesNamespaces < ActiveRecord::Migration
DOWNTIME = false
INDEX_NAME = 'kubernetes_namespaces_cluster_and_namespace'
def change
create_table :clusters_kubernetes_namespaces, id: :bigserial do |t|
t.references :cluster, null: false, index: true, foreign_key: { on_delete: :cascade }
t.references :project, index: true, foreign_key: { on_delete: :nullify }
t.references :cluster_project, index: true, foreign_key: { on_delete: :nullify }
t.timestamps_with_timezone null: false
t.string :encrypted_service_account_token_iv
t.string :namespace, null: false
t.string :service_account_name
t.text :encrypted_service_account_token
t.index [:cluster_id, :namespace], name: INDEX_NAME, unique: true
end
end
end
......@@ -810,6 +810,23 @@ 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", id: :bigserial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "project_id"
t.integer "cluster_project_id"
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.text "encrypted_service_account_token"
t.string "encrypted_service_account_token_iv"
t.string "namespace", null: false
t.string "service_account_name"
end
add_index "clusters_kubernetes_namespaces", ["cluster_id", "namespace"], name: "kubernetes_namespaces_cluster_and_namespace", unique: true, using: :btree
add_index "clusters_kubernetes_namespaces", ["cluster_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_id", using: :btree
add_index "clusters_kubernetes_namespaces", ["cluster_project_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_project_id", using: :btree
add_index "clusters_kubernetes_namespaces", ["project_id"], name: "index_clusters_kubernetes_namespaces_on_project_id", using: :btree
create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
......@@ -3165,6 +3182,9 @@ ActiveRecord::Schema.define(version: 20181013005024) do
add_foreign_key "clusters_applications_prometheus", "clusters", 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: :nullify
add_foreign_key "clusters_kubernetes_namespaces", "clusters", on_delete: :cascade
add_foreign_key "clusters_kubernetes_namespaces", "projects", on_delete: :nullify
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
......
# frozen_string_literal: true
module Gitlab
class NamespaceSanitizer
def self.sanitize(namespace)
namespace.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :cluster_kubernetes_namespace, class: Clusters::KubernetesNamespace do
cluster
project
cluster_project
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :cluster_project, class: Clusters::Project do
cluster
project
end
end
......@@ -11,6 +11,9 @@ describe Clusters::Cluster do
it { is_expected.to have_one(:application_ingress) }
it { is_expected.to have_one(:application_prometheus) }
it { is_expected.to have_one(:application_runner) }
it { is_expected.to have_many(:kubernetes_namespaces) }
it { is_expected.to have_one(:kubernetes_namespace) }
it { is_expected.to delegate_method(:status).to(:provider) }
it { is_expected.to delegate_method(:status_reason).to(:provider) }
it { is_expected.to delegate_method(:status_name).to(:provider) }
......@@ -20,6 +23,7 @@ describe Clusters::Cluster do
it { is_expected.to delegate_method(:available?).to(:application_helm).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_ingress).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_prometheus).with_prefix }
it { is_expected.to respond_to :project }
describe '.enabled' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::KubernetesNamespace, type: :model do
it { is_expected.to belong_to(:cluster_project) }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:cluster) }
it { is_expected.to have_one(:platform_kubernetes) }
describe 'namespace uniqueness validation' do
let(:cluster_project) { create(:cluster_project) }
let(:kubernetes_namespace) do
build(:cluster_kubernetes_namespace,
cluster: cluster_project.cluster,
project: cluster_project.project,
cluster_project: cluster_project)
end
subject { kubernetes_namespace }
context 'when cluster is using the namespace' do
before do
create(:cluster_kubernetes_namespace,
cluster: cluster_project.cluster,
project: cluster_project.project,
cluster_project: cluster_project,
namespace: kubernetes_namespace.namespace)
end
it { is_expected.not_to be_valid }
end
context 'when cluster is not using the namespace' do
it { is_expected.to be_valid }
end
end
describe '#set_namespace_and_service_account_to_default' do
let(:cluster) { platform.cluster }
let(:cluster_project) { create(:cluster_project, cluster: cluster) }
let(:kubernetes_namespace) do
create(:cluster_kubernetes_namespace,
cluster: cluster_project.cluster,
project: cluster_project.project,
cluster_project: cluster_project)
end
describe 'namespace' do
let(:platform) { create(:cluster_platform_kubernetes, namespace: namespace) }
subject { kubernetes_namespace.namespace }
context 'when platform has a namespace assigned' do
let(:namespace) { 'platform-namespace' }
it 'should copy the namespace' do
is_expected.to eq('platform-namespace')
end
end
context 'when platform does not have namespace assigned' do
let(:namespace) { nil }
it 'should set default namespace' do
project_slug = "#{cluster_project.project.path}-#{cluster_project.project_id}"
is_expected.to eq(project_slug)
end
end
end
describe 'service_account_name' do
let(:platform) { create(:cluster_platform_kubernetes) }
subject { kubernetes_namespace.service_account_name }
it 'should set a service account name based on namespace' do
is_expected.to eq("#{kubernetes_namespace.namespace}-service-account")
end
end
end
end
......@@ -9,6 +9,15 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_kind_of(ReactiveCaching) }
it { is_expected.to respond_to :ca_pem }
it { is_expected.to validate_exclusion_of(:namespace).in_array(%w(gitlab-managed-apps)) }
it { is_expected.to validate_presence_of(:api_url) }
it { is_expected.to validate_presence_of(:token) }
it { is_expected.to delegate_method(:project).to(:cluster) }
it { is_expected.to delegate_method(:enabled?).to(:cluster) }
it { is_expected.to delegate_method(:managed?).to(:cluster) }
it { is_expected.to delegate_method(:kubernetes_namespace).to(:cluster) }
describe 'before_validation' do
context 'when namespace includes upper case' do
let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
......@@ -90,6 +99,28 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { expect(kubernetes.save).to be_falsey }
end
end
describe 'when using 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.not_to be_valid }
end
end
end
describe '#kubeclient' do
......@@ -117,41 +148,39 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end
describe '#actual_namespace' do
subject { kubernetes.actual_namespace }
let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
let(:cluster) { create(:cluster, :project) }
let(:project) { cluster.project }
let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
context 'when namespace is present' do
let(:platform) do
create(:cluster_platform_kubernetes,
cluster: cluster,
namespace: namespace)
end
subject { platform.actual_namespace }
context 'with a namespace assigned' do
let(:namespace) { 'namespace-123' }
it { is_expected.to eq(namespace) }
end
context 'when namespace is not present' do
context 'with no namespace assigned' do
let(:namespace) { nil }
it { is_expected.to eq("#{project.path}-#{project.id}") }
end
end
describe '#default_namespace' do
subject { kubernetes.send(:default_namespace) }
context 'when kubernetes namespace is present' do
let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster) }
let(:kubernetes) { create(:cluster_platform_kubernetes, :configured) }
before do
kubernetes_namespace
end
context 'when cluster belongs to a project' do
let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
let(:project) { cluster.project }
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
context 'when cluster belongs to nothing' do
let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) }
it { is_expected.to be_nil }
end
end
......
......@@ -3,4 +3,6 @@ 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) }
it { is_expected.to have_one(:kubernetes_namespace) }
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