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.
This commit is contained in:
parent
a310a638db
commit
e3ca493876
|
|
@ -31,6 +31,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
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
# 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
|
||||
|
|
@ -7,6 +7,8 @@ module Clusters
|
|||
include ReactiveCaching
|
||||
include EnumWithNil
|
||||
|
||||
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] }
|
||||
|
||||
|
|
@ -32,6 +34,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
|
||||
|
|
@ -45,6 +49,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?
|
||||
|
||||
|
|
@ -116,10 +121,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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Introduce new model to persist specific cluster information
|
||||
merge_request: 22404
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# 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
|
||||
20
db/schema.rb
20
db/schema.rb
|
|
@ -691,6 +691,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
|
||||
|
|
@ -2325,6 +2342,9 @@ 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: :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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
class NamespaceSanitizer
|
||||
def self.sanitize(namespace)
|
||||
namespace.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :cluster_kubernetes_namespace, class: Clusters::KubernetesNamespace do
|
||||
cluster
|
||||
project
|
||||
cluster_project
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
# 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
|
||||
context 'when kubernetes namespace is present' do
|
||||
let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster) }
|
||||
|
||||
describe '#default_namespace' do
|
||||
subject { kubernetes.send(:default_namespace) }
|
||||
before do
|
||||
kubernetes_namespace
|
||||
end
|
||||
|
||||
let(:kubernetes) { create(:cluster_platform_kubernetes, :configured) }
|
||||
it { is_expected.to eq(kubernetes_namespace.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("#{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 }
|
||||
context 'when kubernetes namespace is not present' do
|
||||
it { is_expected.to eq("#{project.path}-#{project.id}") }
|
||||
end
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue