From e3ca493876ab71ed29817a0af436fc563f564bbe Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Tue, 16 Oct 2018 15:03:59 -0500 Subject: [PATCH] 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. --- app/models/clusters/cluster.rb | 3 + app/models/clusters/kubernetes_namespace.rb | 49 +++++++++++ app/models/clusters/platforms/kubernetes.rb | 16 +++- app/models/clusters/project.rb | 3 + .../51716-add-kubernetes-namespace-model.yml | 5 ++ ...8_create_clusters_kubernetes_namespaces.rb | 24 ++++++ db/schema.rb | 20 +++++ lib/gitlab/namespace_sanitizer.rb | 9 ++ .../clusters/kubernetes_namespaces.rb | 9 ++ spec/factories/clusters/projects.rb | 8 ++ spec/models/clusters/cluster_spec.rb | 4 + .../clusters/kubernetes_namespace_spec.rb | 84 +++++++++++++++++++ .../clusters/platforms/kubernetes_spec.rb | 75 ++++++++++++----- spec/models/clusters/project_spec.rb | 2 + 14 files changed, 287 insertions(+), 24 deletions(-) create mode 100644 app/models/clusters/kubernetes_namespace.rb create mode 100644 changelogs/unreleased/51716-add-kubernetes-namespace-model.yml create mode 100644 db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb create mode 100644 lib/gitlab/namespace_sanitizer.rb create mode 100644 spec/factories/clusters/kubernetes_namespaces.rb create mode 100644 spec/factories/clusters/projects.rb create mode 100644 spec/models/clusters/kubernetes_namespace_spec.rb diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 20d53b8e620..95efecfc41d 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -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 diff --git a/app/models/clusters/kubernetes_namespace.rb b/app/models/clusters/kubernetes_namespace.rb new file mode 100644 index 00000000000..fb5f6b65d9d --- /dev/null +++ b/app/models/clusters/kubernetes_namespace.rb @@ -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 diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 3a335909101..e8e943872de 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -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') diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb index 839ce796081..15092b1c9d2 100644 --- a/app/models/clusters/project.rb +++ b/app/models/clusters/project.rb @@ -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 diff --git a/changelogs/unreleased/51716-add-kubernetes-namespace-model.yml b/changelogs/unreleased/51716-add-kubernetes-namespace-model.yml new file mode 100644 index 00000000000..ad43c512ba3 --- /dev/null +++ b/changelogs/unreleased/51716-add-kubernetes-namespace-model.yml @@ -0,0 +1,5 @@ +--- +title: Introduce new model to persist specific cluster information +merge_request: 22404 +author: +type: added diff --git a/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb b/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb new file mode 100644 index 00000000000..a58c190e1d6 --- /dev/null +++ b/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 3f3bec0ce04..50989960aa9 100644 --- a/db/schema.rb +++ b/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 diff --git a/lib/gitlab/namespace_sanitizer.rb b/lib/gitlab/namespace_sanitizer.rb new file mode 100644 index 00000000000..d755bbbcaf9 --- /dev/null +++ b/lib/gitlab/namespace_sanitizer.rb @@ -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 diff --git a/spec/factories/clusters/kubernetes_namespaces.rb b/spec/factories/clusters/kubernetes_namespaces.rb new file mode 100644 index 00000000000..6fdada75a3d --- /dev/null +++ b/spec/factories/clusters/kubernetes_namespaces.rb @@ -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 diff --git a/spec/factories/clusters/projects.rb b/spec/factories/clusters/projects.rb new file mode 100644 index 00000000000..6cda77c6f85 --- /dev/null +++ b/spec/factories/clusters/projects.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :cluster_project, class: Clusters::Project do + cluster + project + end +end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 34d321ec604..f5c4b0b66ae 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -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 diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb new file mode 100644 index 00000000000..dea58fa26c7 --- /dev/null +++ b/spec/models/clusters/kubernetes_namespace_spec.rb @@ -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 diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index 66198d5ee2b..e13eb554add 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -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("#{project.path}-#{project.id}") } - 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 diff --git a/spec/models/clusters/project_spec.rb b/spec/models/clusters/project_spec.rb index 7d75d6ab345..82ef5a23c18 100644 --- a/spec/models/clusters/project_spec.rb +++ b/spec/models/clusters/project_spec.rb @@ -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 -- GitLab