Commit 36a01a88 authored by Tiger Watson's avatar Tiger Watson Committed by Thong Kuah

Use separate Kubernetes namespaces per environment

Kubernetes deployments on new clusters will now have
a separate namespace per project environment, instead
of sharing a single namespace for the project.

Behaviour of existing clusters is unchanged.

All new functionality is controlled by the
:kubernetes_namespace_per_environment feature flag,
which is safe to enable/disable at any time.
parent 54377159
...@@ -13,11 +13,11 @@ module Clusters ...@@ -13,11 +13,11 @@ module Clusters
self.reactive_cache_key = ->(finder) { finder.model_name } self.reactive_cache_key = ->(finder) { finder.model_name }
self.reactive_cache_worker_finder = ->(_id, *cache_args) { from_cache(*cache_args) } self.reactive_cache_worker_finder = ->(_id, *cache_args) { from_cache(*cache_args) }
attr_reader :cluster, :project attr_reader :cluster, :environment
def initialize(cluster, project) def initialize(cluster, environment)
@cluster = cluster @cluster = cluster
@project = project @environment = environment
end end
def with_reactive_cache_memoized(*cache_args, &block) def with_reactive_cache_memoized(*cache_args, &block)
...@@ -30,11 +30,11 @@ module Clusters ...@@ -30,11 +30,11 @@ module Clusters
clear_reactive_cache!(*cache_args) clear_reactive_cache!(*cache_args)
end end
def self.from_cache(cluster_id, project_id) def self.from_cache(cluster_id, environment_id)
cluster = Clusters::Cluster.find(cluster_id) cluster = Clusters::Cluster.find(cluster_id)
project = ::Project.find(project_id) environment = Environment.find(environment_id)
new(cluster, project) new(cluster, environment)
end end
def calculate_reactive_cache(*) def calculate_reactive_cache(*)
...@@ -56,7 +56,7 @@ module Clusters ...@@ -56,7 +56,7 @@ module Clusters
end end
def cache_args def cache_args
[cluster.id, project.id] [cluster.id, environment.id]
end end
def service_pod_details(service) def service_pod_details(service)
...@@ -84,7 +84,7 @@ module Clusters ...@@ -84,7 +84,7 @@ module Clusters
private private
def search_namespace def search_namespace
@search_namespace ||= cluster.kubernetes_namespace_for(project) @search_namespace ||= cluster.kubernetes_namespace_for(environment)
end end
def knative_client def knative_client
......
# frozen_string_literal: true
module Clusters
class KubernetesNamespaceFinder
attr_reader :cluster, :project, :environment_slug
def initialize(cluster, project:, environment_slug:, allow_blank_token: false)
@cluster = cluster
@project = project
@environment_slug = environment_slug
@allow_blank_token = allow_blank_token
end
def execute
find_namespace(with_environment: cluster.namespace_per_environment?)
end
private
attr_reader :allow_blank_token
def find_namespace(with_environment:)
relation = with_environment ? namespaces.with_environment_slug(environment_slug) : namespaces
relation.find_by_project_id(project.id)
end
def namespaces
if allow_blank_token
cluster.kubernetes_namespaces
else
cluster.kubernetes_namespaces.has_service_account_token
end
end
end
end
...@@ -3,10 +3,11 @@ ...@@ -3,10 +3,11 @@
module Projects module Projects
module Serverless module Serverless
class FunctionsFinder class FunctionsFinder
include Gitlab::Utils::StrongMemoize
attr_reader :project attr_reader :project
def initialize(project) def initialize(project)
@clusters = project.clusters
@project = project @project = project
end end
...@@ -16,9 +17,8 @@ module Projects ...@@ -16,9 +17,8 @@ module Projects
# Possible return values: Clusters::KnativeServicesFinder::KNATIVE_STATE # Possible return values: Clusters::KnativeServicesFinder::KNATIVE_STATE
def knative_installed def knative_installed
states = @clusters.map do |cluster| states = services_finders.map do |finder|
cluster.application_knative finder.knative_detected.tap do |state|
cluster.knative_services_finder(project).knative_detected.tap do |state|
return state if state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['checking'] # rubocop:disable Cop/AvoidReturnFromBlocks return state if state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['checking'] # rubocop:disable Cop/AvoidReturnFromBlocks
end end
end end
...@@ -31,66 +31,70 @@ module Projects ...@@ -31,66 +31,70 @@ module Projects
end end
def invocation_metrics(environment_scope, name) def invocation_metrics(environment_scope, name)
return unless prometheus_adapter&.can_query? environment = finders_for_scope(environment_scope).first&.environment
cluster = @clusters.find do |c| if environment.present? && environment.prometheus_adapter&.can_query?
environment_scope == c.environment_scope func = ::Serverless::Function.new(project, name, environment.deployment_namespace)
environment.prometheus_adapter.query(:knative_invocation, func)
end end
func = ::Serverless::Function.new(project, name, cluster.kubernetes_namespace_for(project))
prometheus_adapter.query(:knative_invocation, func)
end end
def has_prometheus?(environment_scope) def has_prometheus?(environment_scope)
@clusters.any? do |cluster| finders_for_scope(environment_scope).any? do |finder|
environment_scope == cluster.environment_scope && cluster.application_prometheus_available? finder.cluster.application_prometheus_available?
end end
end end
private private
def knative_service(environment_scope, name) def knative_service(environment_scope, name)
@clusters.map do |cluster| finders_for_scope(environment_scope).map do |finder|
next if environment_scope != cluster.environment_scope services = finder
services = cluster
.knative_services_finder(project)
.services .services
.select { |svc| svc["metadata"]["name"] == name } .select { |svc| svc["metadata"]["name"] == name }
add_metadata(cluster, services).first unless services.nil? add_metadata(finder, services).first unless services.nil?
end end
end end
def knative_services def knative_services
@clusters.map do |cluster| services_finders.map do |finder|
services = cluster services = finder.services
.knative_services_finder(project)
.services
add_metadata(cluster, services) unless services.nil? add_metadata(finder, services) unless services.nil?
end end
end end
def add_metadata(cluster, services) def add_metadata(finder, services)
add_pod_count = services.one?
services.each do |s| services.each do |s|
s["environment_scope"] = cluster.environment_scope s["environment_scope"] = finder.cluster.environment_scope
s["cluster_id"] = cluster.id s["cluster_id"] = finder.cluster.id
if services.length == 1 if add_pod_count
s["podcount"] = cluster s["podcount"] = finder
.knative_services_finder(project)
.service_pod_details(s["metadata"]["name"]) .service_pod_details(s["metadata"]["name"])
.length .length
end end
end end
end end
# rubocop: disable CodeReuse/ServiceClass def services_finders
def prometheus_adapter strong_memoize(:services_finders) do
@prometheus_adapter ||= ::Prometheus::AdapterService.new(project).prometheus_adapter available_environments.map(&:knative_services_finder).compact
end
end
def available_environments
@project.environments.available.preload_cluster
end
def finders_for_scope(environment_scope)
services_finders.select do |finder|
environment_scope == finder.cluster.environment_scope
end
end end
# rubocop: enable CodeReuse/ServiceClass
end end
end end
end end
...@@ -53,6 +53,7 @@ module Clusters ...@@ -53,6 +53,7 @@ module Clusters
validates :name, cluster_name: true validates :name, cluster_name: true
validates :cluster_type, presence: true validates :cluster_type, presence: true
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
validates :namespace_per_environment, inclusion: { in: [true, false] }
validate :restrict_modification, on: :update validate :restrict_modification, on: :update
validate :no_groups, unless: :group_type? validate :no_groups, unless: :group_type?
...@@ -100,16 +101,6 @@ module Clusters ...@@ -100,16 +101,6 @@ module Clusters
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) } scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
scope :with_knative_installed, -> { joins(:application_knative).merge(Clusters::Applications::Knative.available) }
scope :preload_knative, -> {
preload(
:kubernetes_namespaces,
:platform_kubernetes,
:application_knative
)
}
def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc) def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc)
return [] if clusterable.is_a?(Instance) return [] if clusterable.is_a?(Instance)
...@@ -177,36 +168,15 @@ module Clusters ...@@ -177,36 +168,15 @@ module Clusters
platform_kubernetes.kubeclient if kubernetes? platform_kubernetes.kubeclient if kubernetes?
end end
## def kubernetes_namespace_for(environment)
# This is subtly different to #find_or_initialize_kubernetes_namespace_for_project project = environment.project
# below because it will ignore any namespaces that have not got a service account persisted_namespace = Clusters::KubernetesNamespaceFinder.new(
# token. This provides a guarantee that any namespace selected here can be used self,
# for cluster operations - a namespace needs to have a service account configured project: project,
# before it it can be used. environment_slug: environment.slug
# ).execute
# This is used for selecting a namespace to use when querying a cluster, or
# generating variables to pass to CI.
def kubernetes_namespace_for(project)
find_or_initialize_kubernetes_namespace_for_project(
project, scope: kubernetes_namespaces.has_service_account_token
).namespace
end
##
# This is subtly different to #kubernetes_namespace_for because it will include
# namespaces that have yet to receive a service account token. This allows
# the namespace configuration process to be repeatable - if a namespace has
# already been created without a token we don't need to create another
# record entirely, just set the token on the pre-existing namespace.
#
# This is used for configuring cluster namespaces.
def find_or_initialize_kubernetes_namespace_for_project(project, scope: kubernetes_namespaces)
attributes = { project: project }
attributes[:cluster_project] = cluster_project if project_type?
scope.find_or_initialize_by(attributes).tap do |namespace| persisted_namespace&.namespace || Gitlab::Kubernetes::DefaultNamespace.new(self, project: project).from_environment_slug(environment.slug)
namespace.set_defaults
end
end end
def allow_user_defined_namespace? def allow_user_defined_namespace?
...@@ -225,10 +195,6 @@ module Clusters ...@@ -225,10 +195,6 @@ module Clusters
end end
end end
def knative_services_finder(project)
@knative_services_finder ||= KnativeServicesFinder.new(self, project)
end
private private
def instance_domain def instance_domain
......
...@@ -9,12 +9,12 @@ module Clusters ...@@ -9,12 +9,12 @@ module Clusters
belongs_to :cluster_project, class_name: 'Clusters::Project' belongs_to :cluster_project, class_name: 'Clusters::Project'
belongs_to :cluster, class_name: 'Clusters::Cluster' belongs_to :cluster, class_name: 'Clusters::Cluster'
belongs_to :project, class_name: '::Project' belongs_to :project, class_name: '::Project'
belongs_to :environment, optional: true
has_one :platform_kubernetes, through: :cluster has_one :platform_kubernetes, through: :cluster
before_validation :set_defaults
validates :namespace, presence: true validates :namespace, presence: true
validates :namespace, uniqueness: { scope: :cluster_id } validates :namespace, uniqueness: { scope: :cluster_id }
validates :environment_id, uniqueness: { scope: [:cluster_id, :project_id] }, allow_nil: true
validates :service_account_name, presence: true validates :service_account_name, presence: true
...@@ -27,6 +27,7 @@ module Clusters ...@@ -27,6 +27,7 @@ module Clusters
algorithm: 'aes-256-cbc' algorithm: 'aes-256-cbc'
scope :has_service_account_token, -> { where.not(encrypted_service_account_token: nil) } scope :has_service_account_token, -> { where.not(encrypted_service_account_token: nil) }
scope :with_environment_slug, -> (slug) { joins(:environment).where(environments: { slug: slug }) }
def token_name def token_name
"#{namespace}-token" "#{namespace}-token"
...@@ -42,34 +43,8 @@ module Clusters ...@@ -42,34 +43,8 @@ module Clusters
end end
end end
def set_defaults
self.namespace ||= default_platform_kubernetes_namespace
self.namespace ||= default_project_namespace
self.service_account_name ||= default_service_account_name
end
private private
def default_service_account_name
return unless namespace
"#{namespace}-service-account"
end
def default_platform_kubernetes_namespace
platform_kubernetes&.namespace.presence
end
def default_project_namespace
Gitlab::NamespaceSanitizer.sanitize(project_slug) if project_slug
end
def project_slug
return unless project
"#{project.path}-#{project.id}".downcase
end
def kubeconfig def kubeconfig
to_kubeconfig( to_kubeconfig(
url: api_url, url: api_url,
......
...@@ -51,11 +51,6 @@ module Clusters ...@@ -51,11 +51,6 @@ module Clusters
delegate :provided_by_user?, to: :cluster, allow_nil: true delegate :provided_by_user?, to: :cluster, allow_nil: true
delegate :allow_user_defined_namespace?, to: :cluster, allow_nil: true delegate :allow_user_defined_namespace?, to: :cluster, allow_nil: true
# This is just to maintain compatibility with KubernetesService, which
# will be removed in https://gitlab.com/gitlab-org/gitlab-ce/issues/39217.
# It can be removed once KubernetesService is gone.
delegate :kubernetes_namespace_for, to: :cluster, allow_nil: true
alias_method :active?, :enabled? alias_method :active?, :enabled?
enum_with_nil authorization_type: { enum_with_nil authorization_type: {
...@@ -66,7 +61,7 @@ module Clusters ...@@ -66,7 +61,7 @@ module Clusters
default_value_for :authorization_type, :rbac default_value_for :authorization_type, :rbac
def predefined_variables(project:) def predefined_variables(project:, environment_name:)
Gitlab::Ci::Variables::Collection.new.tap do |variables| Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'KUBE_URL', value: api_url) variables.append(key: 'KUBE_URL', value: api_url)
...@@ -77,15 +72,14 @@ module Clusters ...@@ -77,15 +72,14 @@ module Clusters
end end
if !cluster.managed? if !cluster.managed?
project_namespace = namespace.presence || "#{project.path}-#{project.id}".downcase namespace = Gitlab::Kubernetes::DefaultNamespace.new(cluster, project: project).from_environment_name(environment_name)
variables variables
.append(key: 'KUBE_URL', value: api_url)
.append(key: 'KUBE_TOKEN', value: token, public: false, masked: true) .append(key: 'KUBE_TOKEN', value: token, public: false, masked: true)
.append(key: 'KUBE_NAMESPACE', value: project_namespace) .append(key: 'KUBE_NAMESPACE', value: namespace)
.append(key: 'KUBECONFIG', value: kubeconfig(project_namespace), public: false, file: true) .append(key: 'KUBECONFIG', value: kubeconfig(namespace), public: false, file: true)
elsif kubernetes_namespace = cluster.kubernetes_namespaces.has_service_account_token.find_by(project: project) elsif kubernetes_namespace = find_persisted_namespace(project, environment_name: environment_name)
variables.concat(kubernetes_namespace.predefined_variables) variables.concat(kubernetes_namespace.predefined_variables)
end end
...@@ -111,6 +105,22 @@ module Clusters ...@@ -111,6 +105,22 @@ module Clusters
private private
##
# Environment slug can be predicted given an environment
# name, so even if the environment isn't persisted yet we
# still know what to look for.
def environment_slug(name)
Gitlab::Slug::Environment.new(name).generate
end
def find_persisted_namespace(project, environment_name:)
Clusters::KubernetesNamespaceFinder.new(
cluster,
project: project,
environment_slug: environment_slug(environment_name)
).execute
end
def kubeconfig(namespace) def kubeconfig(namespace)
to_kubeconfig( to_kubeconfig(
url: api_url, url: api_url,
......
...@@ -48,6 +48,7 @@ class Environment < ApplicationRecord ...@@ -48,6 +48,7 @@ class Environment < ApplicationRecord
end end
scope :in_review_folder, -> { where(environment_type: "review") } scope :in_review_folder, -> { where(environment_type: "review") }
scope :for_name, -> (name) { where(name: name) } scope :for_name, -> (name) { where(name: name) }
scope :preload_cluster, -> { preload(last_deployment: :cluster) }
## ##
# Search environments which have names like the given query. # Search environments which have names like the given query.
...@@ -170,7 +171,7 @@ class Environment < ApplicationRecord ...@@ -170,7 +171,7 @@ class Environment < ApplicationRecord
def deployment_namespace def deployment_namespace
strong_memoize(:kubernetes_namespace) do strong_memoize(:kubernetes_namespace) do
deployment_platform&.kubernetes_namespace_for(project) deployment_platform.cluster.kubernetes_namespace_for(self) if deployment_platform
end end
end end
...@@ -233,6 +234,12 @@ class Environment < ApplicationRecord ...@@ -233,6 +234,12 @@ class Environment < ApplicationRecord
end end
end end
def knative_services_finder
if last_deployment&.cluster
Clusters::KnativeServicesFinder.new(last_deployment.cluster, self)
end
end
private private
def generate_slug def generate_slug
......
...@@ -1855,8 +1855,12 @@ class Project < ApplicationRecord ...@@ -1855,8 +1855,12 @@ class Project < ApplicationRecord
end end
end end
def deployment_variables(environment: nil) def deployment_variables(environment:)
deployment_platform(environment: environment)&.predefined_variables(project: self) || [] platform = deployment_platform(environment: environment)
return [] unless platform.present?
platform.predefined_variables(project: self, environment_name: environment)
end end
def auto_devops_variables def auto_devops_variables
......
...@@ -24,7 +24,7 @@ class MockDeploymentService < Service ...@@ -24,7 +24,7 @@ class MockDeploymentService < Service
%w() %w()
end end
def predefined_variables(project:) def predefined_variables(project:, environment_name:)
[] []
end end
......
# frozen_string_literal: true
module Clusters
class BuildKubernetesNamespaceService
attr_reader :cluster, :environment
def initialize(cluster, environment:)
@cluster = cluster
@environment = environment
end
def execute
cluster.kubernetes_namespaces.build(attributes)
end
private
def attributes
attributes = {
project: environment.project,
namespace: namespace,
service_account_name: "#{namespace}-service-account"
}
attributes[:cluster_project] = cluster.cluster_project if cluster.project_type?
attributes[:environment] = environment if cluster.namespace_per_environment?
attributes
end
def namespace
Gitlab::Kubernetes::DefaultNamespace.new(cluster, project: environment.project).from_environment_slug(environment.slug)
end
end
end
...@@ -11,7 +11,8 @@ module Clusters ...@@ -11,7 +11,8 @@ module Clusters
def execute(access_token: nil) def execute(access_token: nil)
raise ArgumentError, 'Unknown clusterable provided' unless clusterable raise ArgumentError, 'Unknown clusterable provided' unless clusterable
cluster_params = params.merge(user: current_user).merge(clusterable_params) cluster_params = params.merge(global_params).merge(clusterable_params)
cluster_params[:provider_gcp_attributes].try do |provider| cluster_params[:provider_gcp_attributes].try do |provider|
provider[:access_token] = access_token provider[:access_token] = access_token
end end
...@@ -35,6 +36,10 @@ module Clusters ...@@ -35,6 +36,10 @@ module Clusters
@clusterable ||= params.delete(:clusterable) @clusterable ||= params.delete(:clusterable)
end end
def global_params
{ user: current_user, namespace_per_environment: Feature.enabled?(:kubernetes_namespace_per_environment, default_enabled: true) }
end
def clusterable_params def clusterable_params
case clusterable case clusterable
when ::Project when ::Project
......
...@@ -11,7 +11,6 @@ module Clusters ...@@ -11,7 +11,6 @@ module Clusters
end end
def execute def execute
configure_kubernetes_namespace
create_project_service_account create_project_service_account
configure_kubernetes_token configure_kubernetes_token
...@@ -22,10 +21,6 @@ module Clusters ...@@ -22,10 +21,6 @@ module Clusters
attr_reader :cluster, :kubernetes_namespace, :platform attr_reader :cluster, :kubernetes_namespace, :platform
def configure_kubernetes_namespace
kubernetes_namespace.set_defaults
end
def create_project_service_account def create_project_service_account
Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService.namespace_creator( Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService.namespace_creator(
platform.kubeclient, platform.kubeclient,
......
---
title: Use separate Kubernetes namespaces per environment
merge_request: 30711
author:
type: added
# frozen_string_literal: true
class AddEnvironmentIdToClustersKubernetesNamespaces < ActiveRecord::Migration[5.1]
DOWNTIME = false
def change
add_reference :clusters_kubernetes_namespaces, :environment,
index: true, type: :bigint, foreign_key: { on_delete: :nullify }
end
end
# frozen_string_literal: true
class IndexClustersKubernetesNamespacesOnEnvironmentId < ActiveRecord::Migration[5.1]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_kubernetes_namespaces_on_cluster_project_environment_id'
disable_ddl_transaction!
def up
add_concurrent_index :clusters_kubernetes_namespaces, [:cluster_id, :project_id, :environment_id], unique: true, name: INDEX_NAME
end
def down
remove_concurrent_index :clusters_kubernetes_namespaces, name: INDEX_NAME
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddNamespacePerEnvironmentFlagToClusters < ActiveRecord::Migration[5.1]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :clusters, :namespace_per_environment, :boolean, default: false
end
def down
remove_column :clusters, :namespace_per_environment
end
end
...@@ -880,6 +880,7 @@ ActiveRecord::Schema.define(version: 2019_08_02_235445) do ...@@ -880,6 +880,7 @@ ActiveRecord::Schema.define(version: 2019_08_02_235445) do
t.integer "cluster_type", limit: 2, default: 3, null: false t.integer "cluster_type", limit: 2, default: 3, null: false
t.string "domain" t.string "domain"
t.boolean "managed", default: true, null: false t.boolean "managed", default: true, null: false
t.boolean "namespace_per_environment", default: false, null: false
t.index ["enabled"], name: "index_clusters_on_enabled" t.index ["enabled"], name: "index_clusters_on_enabled"
t.index ["user_id"], name: "index_clusters_on_user_id" t.index ["user_id"], name: "index_clusters_on_user_id"
end end
...@@ -984,9 +985,12 @@ ActiveRecord::Schema.define(version: 2019_08_02_235445) do ...@@ -984,9 +985,12 @@ ActiveRecord::Schema.define(version: 2019_08_02_235445) do
t.string "encrypted_service_account_token_iv" t.string "encrypted_service_account_token_iv"
t.string "namespace", null: false t.string "namespace", null: false
t.string "service_account_name" t.string "service_account_name"
t.bigint "environment_id"
t.index ["cluster_id", "namespace"], name: "kubernetes_namespaces_cluster_and_namespace", unique: true t.index ["cluster_id", "namespace"], name: "kubernetes_namespaces_cluster_and_namespace", unique: true
t.index ["cluster_id", "project_id", "environment_id"], name: "index_kubernetes_namespaces_on_cluster_project_environment_id", unique: true
t.index ["cluster_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_id" t.index ["cluster_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_id"
t.index ["cluster_project_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_project_id" t.index ["cluster_project_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_project_id"
t.index ["environment_id"], name: "index_clusters_kubernetes_namespaces_on_environment_id"
t.index ["project_id"], name: "index_clusters_kubernetes_namespaces_on_project_id" t.index ["project_id"], name: "index_clusters_kubernetes_namespaces_on_project_id"
end end
...@@ -3711,6 +3715,7 @@ ActiveRecord::Schema.define(version: 2019_08_02_235445) do ...@@ -3711,6 +3715,7 @@ ActiveRecord::Schema.define(version: 2019_08_02_235445) do
add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade 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", "cluster_projects", on_delete: :nullify
add_foreign_key "clusters_kubernetes_namespaces", "clusters", on_delete: :cascade add_foreign_key "clusters_kubernetes_namespaces", "clusters", on_delete: :cascade
add_foreign_key "clusters_kubernetes_namespaces", "environments", on_delete: :nullify
add_foreign_key "clusters_kubernetes_namespaces", "projects", on_delete: :nullify add_foreign_key "clusters_kubernetes_namespaces", "projects", on_delete: :nullify
add_foreign_key "container_repositories", "projects" add_foreign_key "container_repositories", "projects"
add_foreign_key "dependency_proxy_blobs", "namespaces", column: "group_id", name: "fk_db58bbc5d7", on_delete: :cascade add_foreign_key "dependency_proxy_blobs", "namespaces", column: "group_id", name: "fk_db58bbc5d7", on_delete: :cascade
......
...@@ -384,13 +384,9 @@ NOTE: **Note:** ...@@ -384,13 +384,9 @@ NOTE: **Note:**
[RBAC](#rbac-cluster-resources) is recommended and the GitLab default. [RBAC](#rbac-cluster-resources) is recommended and the GitLab default.
GitLab creates the necessary service accounts and privileges to install and run GitLab creates the necessary service accounts and privileges to install and run
[GitLab managed applications](#installing-applications). When GitLab creates the cluster: [GitLab managed applications](#installing-applications). When GitLab creates the cluster,
a `gitlab` service account with `cluster-admin` privileges is created in the `default` namespace
- A `gitlab` service account with `cluster-admin` privileges is created in the `default` namespace to manage the newly created cluster.
to manage the newly created cluster.
- A project service account with [`edit`
privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
is created in the GitLab-created project namespace for [deployment jobs](#deployment-variables).
NOTE: **Note:** NOTE: **Note:**
Restricted service account for deployment was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/51716) in GitLab 11.5. Restricted service account for deployment was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/51716) in GitLab 11.5.
...@@ -412,32 +408,37 @@ The resources created by GitLab differ depending on the type of cluster. ...@@ -412,32 +408,37 @@ The resources created by GitLab differ depending on the type of cluster.
GitLab creates the following resources for ABAC clusters. GitLab creates the following resources for ABAC clusters.
| Name | Type | Details | Created when | | Name | Type | Details | Created when |
|:------------------|:---------------------|:----------------------------------|:---------------------------| |:----------------------|:---------------------|:-------------------------------------|:---------------------------|
| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster | | `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster | | `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller | | `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller | | `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
| Project namespace | `ServiceAccount` | Uses namespace of Project | Deploying to a cluster | | Environment namespace | `Namespace` | Contains all environment-specific resources | Deploying to a cluster |
| Project namespace | `Secret` | Token for project ServiceAccount | Deploying to a cluster | | Environment namespace | `ServiceAccount` | Uses namespace of environment | Deploying to a cluster |
| Environment namespace | `Secret` | Token for environment ServiceAccount | Deploying to a cluster |
#### RBAC cluster resources #### RBAC cluster resources
GitLab creates the following resources for RBAC clusters. GitLab creates the following resources for RBAC clusters.
| Name | Type | Details | Created when | | Name | Type | Details | Created when |
|:------------------|:---------------------|:-----------------------------------------------------------------------------------------------------------|:---------------------------| |:----------------------|:---------------------|:-----------------------------------------------------------------------------------------------------------|:---------------------------|
| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster | | `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
| `gitlab-admin` | `ClusterRoleBinding` | [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Creating a new GKE Cluster | | `gitlab-admin` | `ClusterRoleBinding` | [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Creating a new GKE Cluster |
| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster | | `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller | | `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller | | `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
| Project namespace | `ServiceAccount` | Uses namespace of Project | Deploying to a cluster | | Environment namespace | `Namespace` | Contains all environment-specific resources | Deploying to a cluster |
| Project namespace | `Secret` | Token for project ServiceAccount | Deploying to a cluster | | Environment namespace | `ServiceAccount` | Uses namespace of environment | Deploying to a cluster |
| Project namespace | `RoleBinding` | [`edit`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Deploying to a cluster | | Environment namespace | `Secret` | Token for environment ServiceAccount | Deploying to a cluster |
| Environment namespace | `RoleBinding` | [`edit`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Deploying to a cluster |
NOTE: **Note:**
Environment-specific resources are only created if your cluster is [managed by GitLab](#gitlab-managed-clusters).
NOTE: **Note:** NOTE: **Note:**
Project-specific resources are only created if your cluster is [managed by GitLab](#gitlab-managed-clusters). If your project was created before GitLab 12.2 it will use a single namespace for all project environments.
#### Security of GitLab Runners #### Security of GitLab Runners
...@@ -640,8 +641,8 @@ GitLab CI/CD build environment. ...@@ -640,8 +641,8 @@ GitLab CI/CD build environment.
| Variable | Description | | Variable | Description |
| -------- | ----------- | | -------- | ----------- |
| `KUBE_URL` | Equal to the API URL. | | `KUBE_URL` | Equal to the API URL. |
| `KUBE_TOKEN` | The Kubernetes token of the [project service account](#access-controls). | | `KUBE_TOKEN` | The Kubernetes token of the [environment service account](#access-controls). |
| `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `<project_name>-<project_id>`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. | | `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `<project_name>-<project_id>-<environment>`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. |
| `KUBE_CA_PEM_FILE` | Path to a file containing PEM data. Only present if a custom CA bundle was specified. | | `KUBE_CA_PEM_FILE` | Path to a file containing PEM data. Only present if a custom CA bundle was specified. |
| `KUBE_CA_PEM` | (**deprecated**) Raw PEM data. Only if a custom CA bundle was specified. | | `KUBE_CA_PEM` | (**deprecated**) Raw PEM data. Only if a custom CA bundle was specified. |
| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. This config also embeds the same token defined in `KUBE_TOKEN` so you likely will only need this variable. This variable name is also automatically picked up by `kubectl` so you won't actually need to reference it explicitly if using `kubectl`. | | `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. This config also embeds the same token defined in `KUBE_TOKEN` so you likely will only need this variable. This variable name is also automatically picked up by `kubectl` so you won't actually need to reference it explicitly if using `kubectl`. |
......
...@@ -434,7 +434,7 @@ The instructions below relate to installing and running Certbot on a Linux serve ...@@ -434,7 +434,7 @@ The instructions below relate to installing and running Certbot on a Linux serve
./certbot-auto certonly --manual --preferred-challenges dns -d '*.<namespace>.example.com' ./certbot-auto certonly --manual --preferred-challenges dns -d '*.<namespace>.example.com'
``` ```
Where `<namespace>` is the namespace created by GitLab for your serverless project (composed of `<projectname+id>`) and Where `<namespace>` is the namespace created by GitLab for your serverless project (composed of `<project_name>-<project_id>-<environment>`) and
`example.com` is the domain being used for your project. If you are unsure what the namespace of your project is, navigate `example.com` is the domain being used for your project. If you are unsure what the namespace of your project is, navigate
to the **Operations > Serverless** page of your project and inspect to the **Operations > Serverless** page of your project and inspect
the endpoint provided for your function/app. the endpoint provided for your function/app.
......
...@@ -8,31 +8,51 @@ module Gitlab ...@@ -8,31 +8,51 @@ module Gitlab
def unmet? def unmet?
deployment_cluster.present? && deployment_cluster.present? &&
deployment_cluster.managed? && deployment_cluster.managed? &&
(kubernetes_namespace.new_record? || kubernetes_namespace.service_account_token.blank?) missing_namespace?
end end
def complete! def complete!
return unless unmet? return unless unmet?
create_or_update_namespace create_namespace
end end
private private
def missing_namespace?
kubernetes_namespace.nil? || kubernetes_namespace.service_account_token.blank?
end
def deployment_cluster def deployment_cluster
build.deployment&.cluster build.deployment&.cluster
end end
def environment
build.deployment.environment
end
def kubernetes_namespace def kubernetes_namespace
strong_memoize(:kubernetes_namespace) do strong_memoize(:kubernetes_namespace) do
deployment_cluster.find_or_initialize_kubernetes_namespace_for_project(build.project) Clusters::KubernetesNamespaceFinder.new(
deployment_cluster,
project: environment.project,
environment_slug: environment.slug,
allow_blank_token: true
).execute
end end
end end
def create_or_update_namespace def create_namespace
Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
cluster: deployment_cluster, cluster: deployment_cluster,
kubernetes_namespace: kubernetes_namespace kubernetes_namespace: kubernetes_namespace || build_namespace_record
).execute
end
def build_namespace_record
Clusters::BuildKubernetesNamespaceService.new(
deployment_cluster,
environment: environment
).execute ).execute
end end
end end
......
# frozen_string_literal: true
module Gitlab
module Kubernetes
class DefaultNamespace
attr_reader :cluster, :project
delegate :platform_kubernetes, to: :cluster
##
# Ideally we would just use an environment record here instead of
# passing a project and name/slug separately, but we need to be able
# to look up namespaces before the environment has been persisted.
def initialize(cluster, project:)
@cluster = cluster
@project = project
end
def from_environment_name(name)
from_environment_slug(generate_slug(name))
end
def from_environment_slug(slug)
default_platform_namespace(slug) || default_project_namespace(slug)
end
private
def default_platform_namespace(slug)
return unless platform_kubernetes&.namespace.present?
if cluster.managed? && cluster.namespace_per_environment?
"#{platform_kubernetes.namespace}-#{slug}"
else
platform_kubernetes.namespace
end
end
def default_project_namespace(slug)
namespace_slug = "#{project.path}-#{project.id}".downcase
if cluster.namespace_per_environment?
namespace_slug += "-#{slug}"
end
Gitlab::NamespaceSanitizer.sanitize(namespace_slug)
end
##
# Environment slug can be predicted given an environment
# name, so even if the environment isn't persisted yet we
# still know what to look for.
def generate_slug(name)
Gitlab::Slug::Environment.new(name).generate
end
end
end
end
...@@ -4,12 +4,9 @@ module Gitlab ...@@ -4,12 +4,9 @@ module Gitlab
module Prometheus module Prometheus
module QueryVariables module QueryVariables
def self.call(environment) def self.call(environment)
deployment_platform = environment.deployment_platform
namespace = deployment_platform&.kubernetes_namespace_for(environment.project) || ''
{ {
ci_environment_slug: environment.slug, ci_environment_slug: environment.slug,
kube_namespace: namespace, kube_namespace: environment.deployment_namespace || '',
environment_filter: %{container_name!="POD",environment="#{environment.slug}"} environment_filter: %{container_name!="POD",environment="#{environment.slug}"}
} }
end end
......
...@@ -10,12 +10,16 @@ describe Projects::Serverless::FunctionsController do ...@@ -10,12 +10,16 @@ describe Projects::Serverless::FunctionsController do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes } let(:service) { cluster.platform_kubernetes }
let(:project) { cluster.project } let(:project) { cluster.project }
let(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) }
let(:knative_services_finder) { environment.knative_services_finder }
let(:namespace) do let(:namespace) do
create(:cluster_kubernetes_namespace, create(:cluster_kubernetes_namespace,
cluster: cluster, cluster: cluster,
cluster_project: cluster.cluster_project, cluster_project: cluster.cluster_project,
project: cluster.cluster_project.project) project: cluster.cluster_project.project,
environment: environment)
end end
before do before do
...@@ -47,12 +51,11 @@ describe Projects::Serverless::FunctionsController do ...@@ -47,12 +51,11 @@ describe Projects::Serverless::FunctionsController do
end end
context 'when cache is ready' do context 'when cache is ready' do
let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
let(:knative_state) { true } let(:knative_state) { true }
before do before do
allow_any_instance_of(Clusters::Cluster) allow(Clusters::KnativeServicesFinder)
.to receive(:knative_services_finder) .to receive(:new)
.and_return(knative_services_finder) .and_return(knative_services_finder)
synchronous_reactive_cache(knative_services_finder) synchronous_reactive_cache(knative_services_finder)
stub_kubeclient_service_pods( stub_kubeclient_service_pods(
...@@ -107,12 +110,12 @@ describe Projects::Serverless::FunctionsController do ...@@ -107,12 +110,12 @@ describe Projects::Serverless::FunctionsController do
context 'valid data', :use_clean_rails_memory_store_caching do context 'valid data', :use_clean_rails_memory_store_caching do
before do before do
stub_kubeclient_service_pods stub_kubeclient_service_pods
stub_reactive_cache(cluster.knative_services_finder(project), stub_reactive_cache(knative_services_finder,
{ {
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
}, },
*cluster.knative_services_finder(project).cache_args) *knative_services_finder.cache_args)
end end
it 'has a valid function name' do it 'has a valid function name' do
...@@ -140,12 +143,12 @@ describe Projects::Serverless::FunctionsController do ...@@ -140,12 +143,12 @@ describe Projects::Serverless::FunctionsController do
describe 'GET #index with data', :use_clean_rails_memory_store_caching do describe 'GET #index with data', :use_clean_rails_memory_store_caching do
before do before do
stub_kubeclient_service_pods stub_kubeclient_service_pods
stub_reactive_cache(cluster.knative_services_finder(project), stub_reactive_cache(knative_services_finder,
{ {
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
}, },
*cluster.knative_services_finder(project).cache_args) *knative_services_finder.cache_args)
end end
it 'has data' do it 'has data' do
......
...@@ -6,6 +6,7 @@ FactoryBot.define do ...@@ -6,6 +6,7 @@ FactoryBot.define do
name 'test-cluster' name 'test-cluster'
cluster_type :project_type cluster_type :project_type
managed true managed true
namespace_per_environment true
factory :cluster_for_group, traits: [:provided_by_gcp, :group] factory :cluster_for_group, traits: [:provided_by_gcp, :group]
...@@ -29,6 +30,10 @@ FactoryBot.define do ...@@ -29,6 +30,10 @@ FactoryBot.define do
end end
end end
trait :namespace_per_environment_disabled do
namespace_per_environment false
end
trait :provided_by_user do trait :provided_by_user do
provider_type :user provider_type :user
platform_type :kubernetes platform_type :kubernetes
......
...@@ -5,12 +5,21 @@ FactoryBot.define do ...@@ -5,12 +5,21 @@ FactoryBot.define do
association :cluster, :project, :provided_by_gcp association :cluster, :project, :provided_by_gcp
after(:build) do |kubernetes_namespace| after(:build) do |kubernetes_namespace|
if kubernetes_namespace.cluster.project_type? cluster = kubernetes_namespace.cluster
cluster_project = kubernetes_namespace.cluster.cluster_project
if cluster.project_type?
cluster_project = cluster.cluster_project
kubernetes_namespace.project = cluster_project.project kubernetes_namespace.project = cluster_project.project
kubernetes_namespace.cluster_project = cluster_project kubernetes_namespace.cluster_project = cluster_project
end end
kubernetes_namespace.namespace ||=
Gitlab::Kubernetes::DefaultNamespace.new(
cluster,
project: kubernetes_namespace.project
).from_environment_slug(kubernetes_namespace.environment&.slug)
kubernetes_namespace.service_account_name ||= "#{kubernetes_namespace.namespace}-service-account"
end end
trait :with_token do trait :with_token do
......
...@@ -39,17 +39,19 @@ describe 'Functions', :js do ...@@ -39,17 +39,19 @@ describe 'Functions', :js do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes } let(:service) { cluster.platform_kubernetes }
let(:project) { cluster.project } let(:project) { cluster.project }
let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) } let(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, :success, cluster: cluster, environment: environment) }
let(:knative_services_finder) { environment.knative_services_finder }
let(:namespace) do let(:namespace) do
create(:cluster_kubernetes_namespace, create(:cluster_kubernetes_namespace,
cluster: cluster, cluster: cluster,
cluster_project: cluster.cluster_project, project: cluster.cluster_project.project,
project: cluster.cluster_project.project) environment: environment)
end end
before do before do
allow_any_instance_of(Clusters::Cluster) allow(Clusters::KnativeServicesFinder)
.to receive(:knative_services_finder) .to receive(:new)
.and_return(knative_services_finder) .and_return(knative_services_finder)
synchronous_reactive_cache(knative_services_finder) synchronous_reactive_cache(knative_services_finder)
stub_kubeclient_knative_services(stub_get_services_options) stub_kubeclient_knative_services(stub_get_services_options)
......
...@@ -7,15 +7,19 @@ describe Clusters::KnativeServicesFinder do ...@@ -7,15 +7,19 @@ describe Clusters::KnativeServicesFinder do
include ReactiveCachingHelpers include ReactiveCachingHelpers
let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes } let(:service) { environment.deployment_platform }
let(:project) { cluster.cluster_project.project } let(:project) { cluster.cluster_project.project }
let(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) }
let(:namespace) do let(:namespace) do
create(:cluster_kubernetes_namespace, create(:cluster_kubernetes_namespace,
cluster: cluster, cluster: cluster,
cluster_project: cluster.cluster_project, project: project,
project: project) environment: environment)
end end
let(:finder) { described_class.new(cluster, environment) }
before do before do
stub_kubeclient_knative_services(namespace: namespace.namespace) stub_kubeclient_knative_services(namespace: namespace.namespace)
stub_kubeclient_service_pods( stub_kubeclient_service_pods(
...@@ -35,7 +39,7 @@ describe Clusters::KnativeServicesFinder do ...@@ -35,7 +39,7 @@ describe Clusters::KnativeServicesFinder do
context 'when using synchronous reactive cache' do context 'when using synchronous reactive cache' do
before do before do
synchronous_reactive_cache(cluster.knative_services_finder(project)) synchronous_reactive_cache(finder)
end end
context 'when there are functions for cluster namespace' do context 'when there are functions for cluster namespace' do
...@@ -60,21 +64,21 @@ describe Clusters::KnativeServicesFinder do ...@@ -60,21 +64,21 @@ describe Clusters::KnativeServicesFinder do
end end
describe '#service_pod_details' do describe '#service_pod_details' do
subject { cluster.knative_services_finder(project).service_pod_details(project.name) } subject { finder.service_pod_details(project.name) }
it_behaves_like 'a cached data' it_behaves_like 'a cached data'
end end
describe '#services' do describe '#services' do
subject { cluster.knative_services_finder(project).services } subject { finder.services }
it_behaves_like 'a cached data' it_behaves_like 'a cached data'
end end
describe '#knative_detected' do describe '#knative_detected' do
subject { cluster.knative_services_finder(project).knative_detected } subject { finder.knative_detected }
before do before do
synchronous_reactive_cache(cluster.knative_services_finder(project)) synchronous_reactive_cache(finder)
end end
context 'when knative is installed' do context 'when knative is installed' do
...@@ -85,7 +89,7 @@ describe Clusters::KnativeServicesFinder do ...@@ -85,7 +89,7 @@ describe Clusters::KnativeServicesFinder do
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
it "discovers knative installation" do it "discovers knative installation" do
expect { subject } expect { subject }
.to change { cluster.kubeclient.knative_client.discovered } .to change { finder.cluster.kubeclient.knative_client.discovered }
.from(false) .from(false)
.to(true) .to(true)
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::KubernetesNamespaceFinder do
let(:finder) do
described_class.new(
cluster,
project: project,
environment_slug: 'production',
allow_blank_token: allow_blank_token
)
end
def create_namespace(environment, with_token: true)
create(:cluster_kubernetes_namespace,
(with_token ? :with_token : :without_token),
cluster: cluster,
project: project,
environment: environment
)
end
describe '#execute' do
let(:production) { create(:environment, project: project, slug: 'production') }
let(:staging) { create(:environment, project: project, slug: 'staging') }
let(:cluster) { create(:cluster, :group, :provided_by_user) }
let(:project) { create(:project) }
let(:allow_blank_token) { false }
subject { finder.execute }
before do
allow(cluster).to receive(:namespace_per_environment?).and_return(namespace_per_environment)
end
context 'cluster supports separate namespaces per environment' do
let(:namespace_per_environment) { true }
context 'no persisted namespace is present' do
it { is_expected.to be_nil }
end
context 'a namespace with an environment is present' do
context 'environment matches' do
let!(:namespace_with_environment) { create_namespace(production) }
it { is_expected.to eq namespace_with_environment }
context 'project cluster' do
let(:cluster) { create(:cluster, :project, :provided_by_user, projects: [project]) }
it { is_expected.to eq namespace_with_environment }
end
context 'service account token is blank' do
let!(:namespace_with_environment) { create_namespace(production, with_token: false) }
it { is_expected.to be_nil }
context 'allow_blank_token is true' do
let(:allow_blank_token) { true }
it { is_expected.to eq namespace_with_environment }
end
end
end
context 'environment does not match' do
let!(:namespace_with_environment) { create_namespace(staging) }
it { is_expected.to be_nil }
end
end
end
context 'cluster does not support separate namespaces per environment' do
let(:namespace_per_environment) { false }
context 'no persisted namespace is present' do
it { is_expected.to be_nil }
end
context 'a legacy namespace with no environment is present' do
let!(:legacy_namespace) { create_namespace(nil) }
it { is_expected.to eq legacy_namespace }
context 'project cluster' do
let(:cluster) { create(:cluster, :project, :provided_by_user, projects: [project]) }
it { is_expected.to eq legacy_namespace }
end
context 'service account token is blank' do
let!(:legacy_namespace) { create_namespace(nil, with_token: false) }
it { is_expected.to be_nil }
context 'allow_blank_token is true' do
let(:allow_blank_token) { true }
it { is_expected.to eq legacy_namespace }
end
end
end
end
end
end
...@@ -11,12 +11,15 @@ describe Projects::Serverless::FunctionsFinder do ...@@ -11,12 +11,15 @@ describe Projects::Serverless::FunctionsFinder do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes } let(:service) { cluster.platform_kubernetes }
let(:project) { cluster.project } let(:project) { cluster.project }
let(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) }
let(:knative_services_finder) { environment.knative_services_finder }
let(:namespace) do let(:namespace) do
create(:cluster_kubernetes_namespace, create(:cluster_kubernetes_namespace,
cluster: cluster, cluster: cluster,
cluster_project: cluster.cluster_project, project: project,
project: cluster.cluster_project.project) environment: environment)
end end
before do before do
...@@ -29,11 +32,9 @@ describe Projects::Serverless::FunctionsFinder do ...@@ -29,11 +32,9 @@ describe Projects::Serverless::FunctionsFinder do
end end
context 'when reactive_caching has finished' do context 'when reactive_caching has finished' do
let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
before do before do
allow_any_instance_of(Clusters::Cluster) allow(Clusters::KnativeServicesFinder)
.to receive(:knative_services_finder) .to receive(:new)
.and_return(knative_services_finder) .and_return(knative_services_finder)
synchronous_reactive_cache(knative_services_finder) synchronous_reactive_cache(knative_services_finder)
end end
...@@ -47,8 +48,6 @@ describe Projects::Serverless::FunctionsFinder do ...@@ -47,8 +48,6 @@ describe Projects::Serverless::FunctionsFinder do
end end
context 'reactive_caching is finished and knative is installed' do context 'reactive_caching is finished and knative is installed' do
let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
it 'returns true' do it 'returns true' do
stub_kubeclient_knative_services(namespace: namespace.namespace) stub_kubeclient_knative_services(namespace: namespace.namespace)
stub_kubeclient_service_pods(nil, namespace: namespace.namespace) stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
...@@ -74,24 +73,24 @@ describe Projects::Serverless::FunctionsFinder do ...@@ -74,24 +73,24 @@ describe Projects::Serverless::FunctionsFinder do
it 'there are functions', :use_clean_rails_memory_store_caching do it 'there are functions', :use_clean_rails_memory_store_caching do
stub_kubeclient_service_pods stub_kubeclient_service_pods
stub_reactive_cache(cluster.knative_services_finder(project), stub_reactive_cache(knative_services_finder,
{ {
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
}, },
*cluster.knative_services_finder(project).cache_args) *knative_services_finder.cache_args)
expect(finder.execute).not_to be_empty expect(finder.execute).not_to be_empty
end end
it 'has a function', :use_clean_rails_memory_store_caching do it 'has a function', :use_clean_rails_memory_store_caching do
stub_kubeclient_service_pods stub_kubeclient_service_pods
stub_reactive_cache(cluster.knative_services_finder(project), stub_reactive_cache(knative_services_finder,
{ {
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
}, },
*cluster.knative_services_finder(project).cache_args) *knative_services_finder.cache_args)
result = finder.service(cluster.environment_scope, cluster.project.name) result = finder.service(cluster.environment_scope, cluster.project.name)
expect(result).not_to be_empty expect(result).not_to be_empty
...@@ -109,7 +108,7 @@ describe Projects::Serverless::FunctionsFinder do ...@@ -109,7 +108,7 @@ describe Projects::Serverless::FunctionsFinder do
let(:finder) { described_class.new(project) } let(:finder) { described_class.new(project) }
before do before do
allow(finder).to receive(:prometheus_adapter).and_return(prometheus_adapter) allow(Prometheus::AdapterService).to receive(:new).and_return(double(prometheus_adapter: prometheus_adapter))
allow(prometheus_adapter).to receive(:query).and_return(prometheus_empty_body('matrix')) allow(prometheus_adapter).to receive(:query).and_return(prometheus_empty_body('matrix'))
end end
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
let(:build) { create(:ci_build) }
describe '#unmet?' do describe '#unmet?' do
let(:build) { create(:ci_build) }
subject { described_class.new(build).unmet? } subject { described_class.new(build).unmet? }
context 'build has no deployment' do context 'build has no deployment' do
...@@ -18,7 +18,6 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do ...@@ -18,7 +18,6 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
context 'build has a deployment' do context 'build has a deployment' do
let!(:deployment) { create(:deployment, deployable: build, cluster: cluster) } let!(:deployment) { create(:deployment, deployable: build, cluster: cluster) }
let(:cluster) { nil }
context 'and a cluster to deploy to' do context 'and a cluster to deploy to' do
let(:cluster) { create(:cluster, :group) } let(:cluster) { create(:cluster, :group) }
...@@ -32,12 +31,17 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do ...@@ -32,12 +31,17 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
end end
context 'and a namespace is already created for this project' do context 'and a namespace is already created for this project' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, cluster: cluster, project: build.project) } let(:kubernetes_namespace) { instance_double(Clusters::KubernetesNamespace, service_account_token: 'token') }
before do
allow(Clusters::KubernetesNamespaceFinder).to receive(:new)
.and_return(double(execute: kubernetes_namespace))
end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
context 'and the service_account_token is blank' do context 'and the service_account_token is blank' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :without_token, cluster: cluster, project: build.project) } let(:kubernetes_namespace) { instance_double(Clusters::KubernetesNamespace, service_account_token: nil) }
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end end
...@@ -45,34 +49,79 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do ...@@ -45,34 +49,79 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
end end
context 'and no cluster to deploy to' do context 'and no cluster to deploy to' do
let(:cluster) { nil }
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
end end
end end
describe '#complete!' do describe '#complete!' do
let!(:deployment) { create(:deployment, deployable: build, cluster: cluster) } let(:build) { create(:ci_build) }
let(:service) { double(execute: true) } let(:prerequisite) { described_class.new(build) }
let(:cluster) { nil }
subject { described_class.new(build).complete! } subject { prerequisite.complete! }
context 'completion is required' do context 'completion is required' do
let(:cluster) { create(:cluster, :group) } let(:cluster) { create(:cluster, :group) }
let(:deployment) { create(:deployment, cluster: cluster) }
let(:service) { double(execute: true) }
let(:kubernetes_namespace) { double }
before do
allow(prerequisite).to receive(:unmet?).and_return(true)
allow(build).to receive(:deployment).and_return(deployment)
end
context 'kubernetes namespace does not exist' do
let(:namespace_builder) { double(execute: kubernetes_namespace)}
it 'creates a kubernetes namespace' do before do
expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService) allow(Clusters::KubernetesNamespaceFinder).to receive(:new)
.to receive(:new) .and_return(double(execute: nil))
.with(cluster: cluster, kubernetes_namespace: instance_of(Clusters::KubernetesNamespace)) end
.and_return(service)
expect(service).to receive(:execute).once it 'creates a namespace using a new record' do
expect(Clusters::BuildKubernetesNamespaceService)
.to receive(:new)
.with(cluster, environment: deployment.environment)
.and_return(namespace_builder)
subject expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService)
.to receive(:new)
.with(cluster: cluster, kubernetes_namespace: kubernetes_namespace)
.and_return(service)
expect(service).to receive(:execute).once
subject
end
end
context 'kubernetes namespace exists (but has no service_account_token)' do
before do
allow(Clusters::KubernetesNamespaceFinder).to receive(:new)
.and_return(double(execute: kubernetes_namespace))
end
it 'creates a namespace using the tokenless record' do
expect(Clusters::BuildKubernetesNamespaceService).not_to receive(:new)
expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService)
.to receive(:new)
.with(cluster: cluster, kubernetes_namespace: kubernetes_namespace)
.and_return(service)
subject
end
end end
end end
context 'completion is not required' do context 'completion is not required' do
before do
allow(prerequisite).to receive(:unmet?).and_return(false)
end
it 'does not create a namespace' do it 'does not create a namespace' do
expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:new) expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:new)
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Kubernetes::DefaultNamespace do
let(:generator) { described_class.new(cluster, project: environment.project) }
describe '#from_environment_name' do
let(:cluster) { create(:cluster) }
let(:environment) { create(:environment) }
subject { generator.from_environment_name(environment.name) }
it 'generates a slug and passes it to #from_environment_slug' do
expect(Gitlab::Slug::Environment).to receive(:new)
.with(environment.name)
.and_return(double(generate: environment.slug))
expect(generator).to receive(:from_environment_slug)
.with(environment.slug)
.and_return(:mock_namespace)
expect(subject).to eq :mock_namespace
end
end
describe '#from_environment_slug' do
let(:platform) { create(:cluster_platform_kubernetes, namespace: platform_namespace) }
let(:cluster) { create(:cluster, platform_kubernetes: platform) }
let(:project) { create(:project, path: "Path-With-Capitals") }
let(:environment) { create(:environment, project: project) }
subject { generator.from_environment_slug(environment.slug) }
context 'namespace per environment is enabled' do
context 'platform namespace is specified' do
let(:platform_namespace) { 'platform-namespace' }
it { is_expected.to eq "#{platform_namespace}-#{environment.slug}" }
context 'cluster is unmanaged' do
let(:cluster) { create(:cluster, :not_managed, platform_kubernetes: platform) }
it { is_expected.to eq platform_namespace }
end
end
context 'platform namespace is blank' do
let(:platform_namespace) { nil }
let(:mock_namespace) { 'mock-namespace' }
it 'constructs a namespace from the project and environment' do
expect(Gitlab::NamespaceSanitizer).to receive(:sanitize)
.with("#{project.path}-#{project.id}-#{environment.slug}".downcase)
.and_return(mock_namespace)
expect(subject).to eq mock_namespace
end
end
end
context 'namespace per environment is disabled' do
let(:cluster) { create(:cluster, :namespace_per_environment_disabled, platform_kubernetes: platform) }
context 'platform namespace is specified' do
let(:platform_namespace) { 'platform-namespace' }
it { is_expected.to eq platform_namespace }
end
context 'platform namespace is blank' do
let(:platform_namespace) { nil }
let(:mock_namespace) { 'mock-namespace' }
it 'constructs a namespace from the project and environment' do
expect(Gitlab::NamespaceSanitizer).to receive(:sanitize)
.with("#{project.path}-#{project.id}".downcase)
.and_return(mock_namespace)
expect(subject).to eq mock_namespace
end
end
end
end
end
...@@ -23,7 +23,7 @@ describe Gitlab::Prometheus::QueryVariables do ...@@ -23,7 +23,7 @@ describe Gitlab::Prometheus::QueryVariables do
context 'with deployment platform' do context 'with deployment platform' do
context 'with project cluster' do context 'with project cluster' do
let(:kube_namespace) { environment.deployment_platform.cluster.kubernetes_namespace_for(project) } let(:kube_namespace) { environment.deployment_namespace }
before do before do
create(:cluster, :project, :provided_by_user, projects: [project]) create(:cluster, :project, :provided_by_user, projects: [project])
...@@ -38,8 +38,8 @@ describe Gitlab::Prometheus::QueryVariables do ...@@ -38,8 +38,8 @@ describe Gitlab::Prometheus::QueryVariables do
let(:project2) { create(:project) } let(:project2) { create(:project) }
let(:kube_namespace) { k8s_ns.namespace } let(:kube_namespace) { k8s_ns.namespace }
let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project) } let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project, environment: environment) }
let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2) } let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2, environment: environment) }
before do before do
group.projects << project group.projects << project
......
...@@ -38,11 +38,6 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do ...@@ -38,11 +38,6 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to respond_to :project } it { is_expected.to respond_to :project }
it do
expect(subject.knative_services_finder(subject.project))
.to be_instance_of(Clusters::KnativeServicesFinder)
end
describe '.enabled' do describe '.enabled' do
subject { described_class.enabled } subject { described_class.enabled }
...@@ -534,60 +529,39 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do ...@@ -534,60 +529,39 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end end
end end
describe '#find_or_initialize_kubernetes_namespace_for_project' do describe '#kubernetes_namespace_for' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:cluster) { create(:cluster, :group) }
let(:project) { cluster.projects.first } let(:environment) { create(:environment) }
subject { cluster.find_or_initialize_kubernetes_namespace_for_project(project) }
context 'kubernetes namespace exists' do
context 'with no service account token' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, project: project, cluster: cluster) }
it { is_expected.to eq kubernetes_namespace }
end
context 'with a service account token' do subject { cluster.kubernetes_namespace_for(environment) }
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, project: project, cluster: cluster) }
it { is_expected.to eq kubernetes_namespace } before do
end expect(Clusters::KubernetesNamespaceFinder).to receive(:new)
end .with(cluster, project: environment.project, environment_slug: environment.slug)
.and_return(double(execute: persisted_namespace))
context 'kubernetes namespace does not exist' do
it 'initializes a new namespace and sets default values' do
expect(subject).to be_new_record
expect(subject.project).to eq project
expect(subject.cluster).to eq cluster
expect(subject.namespace).to be_present
expect(subject.service_account_name).to be_present
end
end end
context 'a custom scope is provided' do context 'a persisted namespace exists' do
let(:scope) { cluster.kubernetes_namespaces.has_service_account_token } let(:persisted_namespace) { create(:cluster_kubernetes_namespace) }
subject { cluster.find_or_initialize_kubernetes_namespace_for_project(project, scope: scope) }
context 'kubernetes namespace exists' do
context 'with no service account token' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, project: project, cluster: cluster) }
it 'initializes a new namespace and sets default values' do it { is_expected.to eq persisted_namespace.namespace }
expect(subject).to be_new_record end
expect(subject.project).to eq project
expect(subject.cluster).to eq cluster
expect(subject.namespace).to be_present
expect(subject.service_account_name).to be_present
end
end
context 'with a service account token' do context 'no persisted namespace exists' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, project: project, cluster: cluster) } let(:persisted_namespace) { nil }
let(:namespace_generator) { double }
let(:default_namespace) { 'a-default-namespace' }
it { is_expected.to eq kubernetes_namespace } before do
end expect(Gitlab::Kubernetes::DefaultNamespace).to receive(:new)
.with(cluster, project: environment.project)
.and_return(namespace_generator)
expect(namespace_generator).to receive(:from_environment_slug)
.with(environment.slug)
.and_return(default_namespace)
end end
it { is_expected.to eq default_namespace }
end end
end end
......
...@@ -24,70 +24,60 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do ...@@ -24,70 +24,60 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
end end
end end
describe 'namespace uniqueness validation' do describe '.with_environment_slug' do
let(:cluster_project) { create(:cluster_project) } let(:cluster) { create(:cluster, :group) }
let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, namespace: 'my-namespace') } let(:environment) { create(:environment, slug: slug) }
subject { kubernetes_namespace } let(:slug) { 'production' }
context 'when cluster is using the namespace' do subject { described_class.with_environment_slug(slug) }
before do
create(:cluster_kubernetes_namespace,
cluster: kubernetes_namespace.cluster,
namespace: 'my-namespace')
end
it { is_expected.not_to be_valid } context 'there is no associated environment' do
end let!(:namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: environment.project) }
context 'when cluster is not using the namespace' do it { is_expected.to be_empty }
it { is_expected.to be_valid }
end end
end
describe '#set_defaults' do context 'there is an assicated environment' do
let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace) } let!(:namespace) do
let(:cluster) { kubernetes_namespace.cluster } create(
let(:platform) { kubernetes_namespace.platform_kubernetes } :cluster_kubernetes_namespace,
cluster: cluster,
subject { kubernetes_namespace.set_defaults } project: environment.project,
environment: environment
describe '#namespace' do )
before do
platform.update_column(:namespace, namespace)
end end
context 'when platform has a namespace assigned' do context 'with a matching slug' do
let(:namespace) { 'platform-namespace' } it { is_expected.to eq [namespace] }
it 'copies the namespace' do
subject
expect(kubernetes_namespace.namespace).to eq('platform-namespace')
end
end end
context 'when platform does not have namespace assigned' do context 'without a matching slug' do
let(:project) { kubernetes_namespace.project } let(:environment) { create(:environment, slug: 'staging') }
let(:namespace) { nil }
let(:project_slug) { "#{project.path}-#{project.id}" }
it 'fallbacks to project namespace' do
subject
expect(kubernetes_namespace.namespace).to eq(project_slug) it { is_expected.to be_empty }
end
end end
end end
end
describe '#service_account_name' do describe 'namespace uniqueness validation' do
let(:service_account_name) { "#{kubernetes_namespace.namespace}-service-account" } let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, namespace: 'my-namespace') }
it 'sets a service account name based on namespace' do subject { kubernetes_namespace }
subject
expect(kubernetes_namespace.service_account_name).to eq(service_account_name) context 'when cluster is using the namespace' do
before do
create(:cluster_kubernetes_namespace,
cluster: kubernetes_namespace.cluster,
environment: kubernetes_namespace.environment,
namespace: 'my-namespace')
end 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
end end
......
...@@ -205,192 +205,77 @@ describe Clusters::Platforms::Kubernetes do ...@@ -205,192 +205,77 @@ describe Clusters::Platforms::Kubernetes do
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end end
describe '#kubernetes_namespace_for' do describe '#predefined_variables' do
let(:cluster) { create(:cluster, :project) } let(:project) { create(:project) }
let(:project) { cluster.project } let(:cluster) { create(:cluster, :group, platform_kubernetes: platform) }
let(:platform) { create(:cluster_platform_kubernetes) }
let(:platform) do let(:persisted_namespace) { create(:cluster_kubernetes_namespace, project: project, cluster: cluster) }
create(:cluster_platform_kubernetes,
cluster: cluster,
namespace: namespace)
end
subject { platform.kubernetes_namespace_for(project) }
context 'with a namespace assigned' do
let(:namespace) { 'namespace-123' }
it { is_expected.to eq(namespace) }
context 'kubernetes namespace is present but has no service account token' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster) }
it { is_expected.to eq(namespace) }
end
end
context 'with no namespace assigned' do
let(:namespace) { nil }
context 'when kubernetes namespace is present' do
let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, cluster: cluster) }
before do
kubernetes_namespace
end
it { is_expected.to eq(kubernetes_namespace.namespace) }
context 'kubernetes namespace has no service account token' do
before do
kubernetes_namespace.update!(namespace: 'old-namespace', service_account_token: nil)
end
it { is_expected.to eq("#{project.path}-#{project.id}") } let(:environment_name) { 'env/production' }
end let(:environment_slug) { Gitlab::Slug::Environment.new(environment_name).generate }
end
context 'when kubernetes namespace is not present' do subject { platform.predefined_variables(project: project, environment_name: environment_name) }
it { is_expected.to eq("#{project.path}-#{project.id}") }
end
end
end
describe '#predefined_variables' do before do
let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } allow(Clusters::KubernetesNamespaceFinder).to receive(:new)
let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem) } .with(cluster, project: project, environment_slug: environment_slug)
let(:api_url) { 'https://kube.domain.com' } .and_return(double(execute: persisted_namespace))
let(:ca_pem) { File.read(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) }
subject { kubernetes.predefined_variables(project: cluster.project) }
shared_examples 'setting variables' do
it 'sets the variables' do
expect(subject).to include(
{ key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_CA_PEM', value: ca_pem, public: true },
{ key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
)
end
end end
context 'kubernetes namespace is created with no service account token' do it { is_expected.to include(key: 'KUBE_URL', value: platform.api_url, public: true) }
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster) }
it_behaves_like 'setting variables' context 'platform has a CA certificate' do
let(:ca_pem) { File.read(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) }
let(:platform) { create(:cluster_platform_kubernetes, ca_cert: ca_pem) }
it 'does not set KUBE_TOKEN' do it { is_expected.to include(key: 'KUBE_CA_PEM', value: ca_pem, public: true) }
expect(subject).not_to include( it { is_expected.to include(key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true) }
{ key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true }
)
end
end end
context 'kubernetes namespace is created with service account token' do context 'kubernetes namespace exists' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, cluster: cluster) } let(:variable) { Hash(key: :fake_key, value: 'fake_value') }
let(:namespace_variables) { Gitlab::Ci::Variables::Collection.new([variable]) }
it_behaves_like 'setting variables'
it 'sets KUBE_TOKEN' do before do
expect(subject).to include( expect(persisted_namespace).to receive(:predefined_variables).and_return(namespace_variables)
{ key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true }
)
end end
context 'the cluster has been set to unmanaged after the namespace was created' do it { is_expected.to include(variable) }
before do
cluster.update!(managed: false)
end
it_behaves_like 'setting variables'
it 'sets KUBE_TOKEN from the platform' do
expect(subject).to include(
{ key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true }
)
end
context 'the platform has a custom namespace set' do
before do
kubernetes.update!(namespace: 'custom-namespace')
end
it 'sets KUBE_NAMESPACE from the platform' do
expect(subject).to include(
{ key: 'KUBE_NAMESPACE', value: kubernetes.namespace, public: true, masked: false }
)
end
end
context 'there is no namespace specified on the platform' do
let(:project) { cluster.project }
before do
kubernetes.update!(namespace: nil)
end
it 'sets KUBE_NAMESPACE to a default for the project' do
expect(subject).to include(
{ key: 'KUBE_NAMESPACE', value: "#{project.path}-#{project.id}", public: true, masked: false }
)
end
end
end
end end
context 'group level cluster' do context 'kubernetes namespace does not exist' do
let!(:cluster) { create(:cluster, :group, platform_kubernetes: kubernetes) } let(:persisted_namespace) { nil }
let(:namespace) { 'kubernetes-namespace' }
let(:project) { create(:project, group: cluster.group) } let(:kubeconfig) { 'kubeconfig' }
subject { kubernetes.predefined_variables(project: project) }
context 'no kubernetes namespace for the project' do
it_behaves_like 'setting variables'
it 'does not return KUBE_TOKEN' do
expect(subject).not_to include(
{ key: 'KUBE_TOKEN', value: kubernetes.token, public: false }
)
end
context 'the cluster is not managed' do
let!(:cluster) { create(:cluster, :group, :not_managed, platform_kubernetes: kubernetes) }
it_behaves_like 'setting variables' before do
allow(Gitlab::Kubernetes::DefaultNamespace).to receive(:new)
it 'sets KUBE_TOKEN' do .with(cluster, project: project).and_return(double(from_environment_name: namespace))
expect(subject).to include( allow(platform).to receive(:kubeconfig).with(namespace).and_return(kubeconfig)
{ key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true }
)
end
end
end end
context 'kubernetes namespace exists for the project' do it { is_expected.not_to include(key: 'KUBE_TOKEN', value: platform.token, public: false, masked: true) }
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, cluster: cluster, project: project) } it { is_expected.not_to include(key: 'KUBE_NAMESPACE', value: namespace) }
it { is_expected.not_to include(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) }
it_behaves_like 'setting variables' context 'cluster is unmanaged' do
let(:cluster) { create(:cluster, :group, :not_managed, platform_kubernetes: platform) }
it 'sets KUBE_TOKEN' do it { is_expected.to include(key: 'KUBE_TOKEN', value: platform.token, public: false, masked: true) }
expect(subject).to include( it { is_expected.to include(key: 'KUBE_NAMESPACE', value: namespace) }
{ key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true } it { is_expected.to include(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) }
)
end
end end
end end
context 'with a domain' do context 'cluster variables' do
let!(:cluster) do let(:variable) { Hash(key: :fake_key, value: 'fake_value') }
create(:cluster, :provided_by_gcp, :with_domain, let(:cluster_variables) { Gitlab::Ci::Variables::Collection.new([variable]) }
platform_kubernetes: kubernetes)
end
it 'sets KUBE_INGRESS_BASE_DOMAIN' do before do
expect(subject).to include( expect(cluster).to receive(:predefined_variables).and_return(cluster_variables)
{ key: 'KUBE_INGRESS_BASE_DOMAIN', value: cluster.domain, public: true }
)
end end
it { is_expected.to include(variable) }
end end
end end
...@@ -410,7 +295,7 @@ describe Clusters::Platforms::Kubernetes do ...@@ -410,7 +295,7 @@ describe Clusters::Platforms::Kubernetes do
end end
context 'with valid pods' do context 'with valid pods' do
let(:pod) { kube_pod(environment_slug: environment.slug, namespace: cluster.kubernetes_namespace_for(project), project_slug: project.full_path_slug) } let(:pod) { kube_pod(environment_slug: environment.slug, namespace: cluster.kubernetes_namespace_for(environment), project_slug: project.full_path_slug) }
let(:pod_with_no_terminal) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: "Pending") } let(:pod_with_no_terminal) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: "Pending") }
let(:terminals) { kube_terminals(service, pod) } let(:terminals) { kube_terminals(service, pod) }
let(:pods) { [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")] } let(:pods) { [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")] }
......
...@@ -575,6 +575,34 @@ describe Environment, :use_clean_rails_memory_store_caching do ...@@ -575,6 +575,34 @@ describe Environment, :use_clean_rails_memory_store_caching do
end end
end end
describe '#deployment_namespace' do
let(:environment) { create(:environment) }
subject { environment.deployment_namespace }
before do
allow(environment).to receive(:deployment_platform).and_return(deployment_platform)
end
context 'no deployment platform available' do
let(:deployment_platform) { nil }
it { is_expected.to be_nil }
end
context 'deployment platform is available' do
let(:cluster) { create(:cluster, :provided_by_user, :project, projects: [environment.project]) }
let(:deployment_platform) { cluster.platform }
it 'retrieves a namespace from the cluster' do
expect(cluster).to receive(:kubernetes_namespace_for)
.with(environment).and_return('mock-namespace')
expect(subject).to eq 'mock-namespace'
end
end
end
describe '#terminals' do describe '#terminals' do
subject { environment.terminals } subject { environment.terminals }
...@@ -823,4 +851,35 @@ describe Environment, :use_clean_rails_memory_store_caching do ...@@ -823,4 +851,35 @@ describe Environment, :use_clean_rails_memory_store_caching do
subject.prometheus_adapter subject.prometheus_adapter
end end
end end
describe '#knative_services_finder' do
let(:environment) { create(:environment) }
subject { environment.knative_services_finder }
context 'environment has no deployments' do
it { is_expected.to be_nil }
end
context 'environment has a deployment' do
let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) }
context 'with no cluster associated' do
let(:cluster) { nil }
it { is_expected.to be_nil }
end
context 'with a cluster associated' do
let(:cluster) { create(:cluster) }
it 'calls the service finder' do
expect(Clusters::KnativeServicesFinder).to receive(:new)
.with(cluster, environment).and_return(:finder)
is_expected.to eq :finder
end
end
end
end
end end
...@@ -2594,45 +2594,33 @@ describe Project do ...@@ -2594,45 +2594,33 @@ describe Project do
end end
describe '#deployment_variables' do describe '#deployment_variables' do
context 'when project has no deployment service' do let(:project) { create(:project) }
let(:project) { create(:project) } let(:environment) { 'production' }
it 'returns an empty array' do subject { project.deployment_variables(environment: environment) }
expect(project.deployment_variables).to eq []
end before do
expect(project).to receive(:deployment_platform).with(environment: environment)
.and_return(deployment_platform)
end end
context 'when project uses mock deployment service' do context 'when project has no deployment platform' do
let(:project) { create(:mock_deployment_project) } let(:deployment_platform) { nil }
it 'returns an empty array' do it { is_expected.to eq [] }
expect(project.deployment_variables).to eq []
end
end end
context 'when project has a deployment service' do context 'when project has a deployment platform' do
context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has not been executed' do let(:platform_variables) { %w(platform variables) }
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:deployment_platform) { double }
let(:project) { cluster.project }
it 'does not return variables from this service' do before do
expect(project.deployment_variables).not_to include( expect(deployment_platform).to receive(:predefined_variables)
{ key: 'KUBE_TOKEN', value: project.deployment_platform.token, public: false, masked: true } .with(project: project, environment_name: environment)
) .and_return(platform_variables)
end
end end
context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has been executed' do it { is_expected.to eq platform_variables }
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token) }
let!(:cluster) { kubernetes_namespace.cluster }
let(:project) { kubernetes_namespace.project }
it 'returns token from kubernetes namespace' do
expect(project.deployment_variables).to include(
{ key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true }
)
end
end
end end
end end
......
...@@ -336,7 +336,6 @@ describe API::ProjectClusters do ...@@ -336,7 +336,6 @@ describe API::ProjectClusters do
it 'does not update cluster attributes' do it 'does not update cluster attributes' do
expect(cluster.domain).not_to eq('new_domain.com') expect(cluster.domain).not_to eq('new_domain.com')
expect(cluster.platform_kubernetes.namespace).not_to eq('invalid_namespace') expect(cluster.platform_kubernetes.namespace).not_to eq('invalid_namespace')
expect(cluster.kubernetes_namespace_for(project)).not_to eq('invalid_namespace')
end end
it 'returns validation errors' do it 'returns validation errors' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::BuildKubernetesNamespaceService do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:environment) { create(:environment) }
let(:project) { environment.project }
let(:namespace_generator) { double(from_environment_slug: namespace) }
let(:namespace) { 'namespace' }
subject { described_class.new(cluster, environment: environment).execute }
before do
allow(Gitlab::Kubernetes::DefaultNamespace).to receive(:new).and_return(namespace_generator)
end
shared_examples 'shared attributes' do
it 'initializes a new namespace and sets default values' do
expect(subject).to be_new_record
expect(subject.cluster).to eq cluster
expect(subject.project).to eq project
expect(subject.namespace).to eq namespace
expect(subject.service_account_name).to eq "#{namespace}-service-account"
end
end
include_examples 'shared attributes'
it 'sets cluster_project and environment' do
expect(subject.cluster_project).to eq cluster.cluster_project
expect(subject.environment).to eq environment
end
context 'namespace per environment is disabled' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp, :namespace_per_environment_disabled) }
include_examples 'shared attributes'
it 'does not set environment' do
expect(subject.cluster_project).to eq cluster.cluster_project
expect(subject.environment).to be_nil
end
end
context 'group cluster' do
let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
include_examples 'shared attributes'
it 'does not set cluster_project' do
expect(subject.cluster_project).to be_nil
expect(subject.environment).to eq environment
end
end
end
...@@ -9,8 +9,9 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d ...@@ -9,8 +9,9 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
let(:platform) { cluster.platform } let(:platform) { cluster.platform }
let(:api_url) { 'https://kubernetes.example.com' } let(:api_url) { 'https://kubernetes.example.com' }
let(:project) { cluster.project } let(:project) { cluster.project }
let(:environment) { create(:environment, project: project) }
let(:cluster_project) { cluster.cluster_project } let(:cluster_project) { cluster.cluster_project }
let(:namespace) { "#{project.path}-#{project.id}" } let(:namespace) { "#{project.name}-#{project.id}-#{environment.slug}" }
subject do subject do
described_class.new( described_class.new(
...@@ -79,7 +80,8 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d ...@@ -79,7 +80,8 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
let(:kubernetes_namespace) do let(:kubernetes_namespace) do
build(:cluster_kubernetes_namespace, build(:cluster_kubernetes_namespace,
cluster: cluster, cluster: cluster,
project: project) project: project,
environment: environment)
end end
it_behaves_like 'successful creation of kubernetes namespace' it_behaves_like 'successful creation of kubernetes namespace'
...@@ -92,20 +94,22 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d ...@@ -92,20 +94,22 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
build(:cluster_kubernetes_namespace, build(:cluster_kubernetes_namespace,
cluster: cluster, cluster: cluster,
project: cluster_project.project, project: cluster_project.project,
cluster_project: cluster_project) cluster_project: cluster_project,
environment: environment)
end end
it_behaves_like 'successful creation of kubernetes namespace' it_behaves_like 'successful creation of kubernetes namespace'
end end
context 'when there is a Kubernetes Namespace associated' do context 'when there is a Kubernetes Namespace associated' do
let(:namespace) { 'new-namespace' } let(:namespace) { "new-namespace-#{environment.slug}" }
let(:kubernetes_namespace) do let(:kubernetes_namespace) do
create(:cluster_kubernetes_namespace, create(:cluster_kubernetes_namespace,
cluster: cluster, cluster: cluster,
project: cluster_project.project, project: cluster_project.project,
cluster_project: cluster_project) cluster_project: cluster_project,
environment: environment)
end end
before do before do
......
...@@ -50,7 +50,7 @@ RSpec.shared_examples 'additional metrics query' do ...@@ -50,7 +50,7 @@ RSpec.shared_examples 'additional metrics query' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project } let(:project) { cluster.project }
let(:environment) { create(:environment, slug: 'environment-slug', project: project) } let(:environment) { create(:environment, slug: 'environment-slug', project: project) }
let(:kube_namespace) { project.deployment_platform.kubernetes_namespace_for(project) } let(:kube_namespace) { environment.deployment_namespace }
it_behaves_like 'query context containing environment slug and filter' it_behaves_like 'query context containing environment slug and filter'
......
...@@ -32,23 +32,56 @@ shared_context 'invalid cluster create params' do ...@@ -32,23 +32,56 @@ shared_context 'invalid cluster create params' do
end end
shared_examples 'create cluster service success' do shared_examples 'create cluster service success' do
it 'creates a cluster object and performs a worker' do context 'namespace per environment feature is enabled' do
expect(ClusterProvisionWorker).to receive(:perform_async) before do
stub_feature_flags(kubernetes_namespace_per_environment: true)
expect { subject } end
.to change { Clusters::Cluster.count }.by(1)
.and change { Clusters::Providers::Gcp.count }.by(1) it 'creates a cluster object and performs a worker' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect(subject.name).to eq('test-cluster')
expect(subject.user).to eq(user) expect { subject }
expect(subject.project).to eq(project) .to change { Clusters::Cluster.count }.by(1)
expect(subject.provider.gcp_project_id).to eq('gcp-project') .and change { Clusters::Providers::Gcp.count }.by(1)
expect(subject.provider.zone).to eq('us-central1-a')
expect(subject.provider.num_nodes).to eq(1) expect(subject.name).to eq('test-cluster')
expect(subject.provider.machine_type).to eq('machine_type-a') expect(subject.user).to eq(user)
expect(subject.provider.access_token).to eq(access_token) expect(subject.project).to eq(project)
expect(subject.provider).to be_legacy_abac expect(subject.provider.gcp_project_id).to eq('gcp-project')
expect(subject.platform).to be_nil expect(subject.provider.zone).to eq('us-central1-a')
expect(subject.provider.num_nodes).to eq(1)
expect(subject.provider.machine_type).to eq('machine_type-a')
expect(subject.provider.access_token).to eq(access_token)
expect(subject.provider).to be_legacy_abac
expect(subject.platform).to be_nil
expect(subject.namespace_per_environment).to eq true
end
end
context 'namespace per environment feature is disabled' do
before do
stub_feature_flags(kubernetes_namespace_per_environment: false)
end
it 'creates a cluster object and performs a worker' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { subject }
.to change { Clusters::Cluster.count }.by(1)
.and change { Clusters::Providers::Gcp.count }.by(1)
expect(subject.name).to eq('test-cluster')
expect(subject.user).to eq(user)
expect(subject.project).to eq(project)
expect(subject.provider.gcp_project_id).to eq('gcp-project')
expect(subject.provider.zone).to eq('us-central1-a')
expect(subject.provider.num_nodes).to eq(1)
expect(subject.provider.machine_type).to eq('machine_type-a')
expect(subject.provider.access_token).to eq(access_token)
expect(subject.provider).to be_legacy_abac
expect(subject.platform).to be_nil
expect(subject.namespace_per_environment).to eq false
end
end end
end 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