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

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 3e3f8ac8
No related branches found
No related tags found
No related merge requests found
Showing
with 277 additions and 25 deletions
Loading
Loading
@@ -31,6 +31,11 @@ module Clusters
has_one :application_runner, class_name: 'Clusters::Applications::Runner'
has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter'
 
has_one :first_cluster_project, -> { order :created_at }, class_name: 'Clusters::Project'
has_many :kubernetes_namespaces, through: :first_cluster_project
alias_method :cluster_project, :first_cluster_project
accepts_nested_attributes_for :provider_gcp, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true
 
Loading
Loading
@@ -47,6 +52,8 @@ module Clusters
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
 
delegate :kubernetes_namespace, to: :cluster_project, allow_nil: true
enum platform_type: {
kubernetes: 1
}
Loading
Loading
# 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
delegate :platform_kubernetes, to: :cluster, allow_nil: true
delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true
validates :namespace, presence: true
validates :namespace, uniqueness: { scope: :cluster_project_id }
before_validation :set_cluster_namespace_and_service_account
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_cluster_namespace_and_service_account
self.namespace = build_kubernetes_namespace
self.service_account_name = build_service_account_name
end
def build_kubernetes_namespace
platform_kubernetes_namespace.presence || default_namespace
end
def build_service_account_name
"#{namespace}-service-account"
end
def platform_kubernetes_namespace
@platform_kubernetes_namespace ||= platform_kubernetes&.namespace
end
def default_namespace
project_slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
def project_slug
"#{project.path}-#{project.id}".downcase
end
end
end
Loading
Loading
@@ -6,6 +6,9 @@ module Clusters
include Gitlab::Kubernetes
include ReactiveCaching
include EnumWithNil
include AfterCommitQueue
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] }
Loading
Loading
@@ -32,6 +35,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
Loading
Loading
@@ -45,6 +50,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?
 
Loading
Loading
@@ -116,6 +122,11 @@ module Clusters
end
 
def default_namespace
kubernetes_namespace&.namespace.presence || fallback_default_namespace
end
# DEPRECATED
def fallback_default_namespace
return unless project
 
slug = "#{project.path}-#{project.id}".downcase
Loading
Loading
@@ -123,7 +134,7 @@ module Clusters
end
 
def build_kube_client!(api_groups: ['api'], api_version: 'v1')
raise "Incomplete settings" unless api_url && actual_namespace
raise "Incomplete settings" unless api_url
 
unless (username && password) || token
raise "Either username/password or token is required to access API"
Loading
Loading
Loading
Loading
@@ -2,9 +2,16 @@
 
module Clusters
class Project < ActiveRecord::Base
include Gitlab::Utils::StrongMemoize
self.table_name = 'cluster_projects'
 
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 :last_kubernetes_namespace, -> { order created_at: :desc }, class_name: 'Clusters::KubernetesNamespace', foreign_key: :cluster_project_id
alias_method :kubernetes_namespace, :last_kubernetes_namespace
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_project_and_namespace'
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 :encrypted_service_account_token_iv
t.string :namespace, null: false
t.string :service_account_name
end
add_index :clusters_kubernetes_namespaces, [:cluster_project_id, :namespace],
unique: true,
name: INDEX_NAME
end
end
Loading
Loading
@@ -691,6 +691,19 @@ 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 "encrypted_service_account_token_iv"
t.string "namespace", null: false
t.string "service_account_name"
end
add_index "clusters_kubernetes_namespaces", ["cluster_project_id", "namespace"], name: "kubernetes_namespaces_cluster_project_and_namespace", unique: true, using: :btree
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
@@ -2325,6 +2338,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
FactoryBot.define do
factory :cluster_kubernetes_namespace, class: Clusters::KubernetesNamespace do
cluster_project
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :cluster_project, class: Clusters::Project do
cluster
project
end
end
Loading
Loading
@@ -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_one(:first_cluster_project) }
it { is_expected.to have_many(:kubernetes_namespaces) }
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) }
Loading
Loading
@@ -20,7 +23,10 @@ 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 delegate_method(:kubernetes_namespace).to(:cluster_project) }
it { is_expected.to respond_to :project }
it { is_expected.to respond_to :cluster_project }
 
describe '.enabled' do
subject { described_class.enabled }
Loading
Loading
# 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 have_one(:project) }
it { is_expected.to have_one(:cluster) }
it { is_expected.to delegate_method(:platform_kubernetes).to(:cluster) }
it { is_expected.to delegate_method(:rbac?).to(:platform_kubernetes).with_prefix }
describe 'namespace uniqueness validation' do
let(:cluster_project) { create(:cluster_project) }
let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, cluster_project: cluster_project) }
subject { kubernetes_namespace }
context 'when cluster project is using the namespace' do
before do
create(:cluster_kubernetes_namespace, cluster_project: cluster_project)
end
it { is_expected.not_to be_valid }
end
context 'when cluster project is not using the namespace' do
it { is_expected.to be_valid }
end
end
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) }
before do
kubernetes_namespace.save
end
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
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
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
Loading
Loading
@@ -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) }
Loading
Loading
@@ -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
Loading
Loading
@@ -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(:namespace) { 'namespace-123' }
it { is_expected.to eq(namespace) }
let(:platform) do
create(:cluster_platform_kubernetes,
cluster: cluster,
namespace: platform_namespace)
end
 
context 'when namespace is not present' do
let(:namespace) { nil }
subject { platform.actual_namespace }
 
it { is_expected.to eq("#{project.path}-#{project.id}") }
end
end
context 'when platform has namespace assigned' do
let(:platform_namespace) { 'namespace-123' }
 
describe '#default_namespace' do
subject { kubernetes.send(:default_namespace) }
it { is_expected.to eq(platform_namespace) }
end
 
let(:kubernetes) { create(:cluster_platform_kubernetes, :configured) }
context 'when platform has no namespace assigned' do
let(:platform_namespace) { nil }
 
context 'when cluster belongs to a project' do
let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
let(:project) { cluster.project }
context 'when kubernetes namespace is present' do
let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster_project: cluster.cluster_project) }
 
it { is_expected.to eq("#{project.path}-#{project.id}") }
end
before do
kubernetes_namespace
end
 
context 'when cluster belongs to nothing' do
let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) }
it { is_expected.to eq(kubernetes_namespace.namespace) }
end
 
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
 
Loading
Loading
Loading
Loading
@@ -3,4 +3,8 @@ 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(:last_kubernetes_namespace) }
it { is_expected.to respond_to(:kubernetes_namespace) }
end
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