Skip to content
Snippets Groups Projects
Commit 36a01a88 authored by Tiger Watson's avatar Tiger Watson Committed by Thong Kuah
Browse files

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
No related branches found
No related tags found
No related merge requests found
Showing
with 634 additions and 357 deletions
# 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
Loading
Loading
@@ -4,12 +4,9 @@ module Gitlab
module Prometheus
module QueryVariables
def self.call(environment)
deployment_platform = environment.deployment_platform
namespace = deployment_platform&.kubernetes_namespace_for(environment.project) || ''
{
ci_environment_slug: environment.slug,
kube_namespace: namespace,
kube_namespace: environment.deployment_namespace || '',
environment_filter: %{container_name!="POD",environment="#{environment.slug}"}
}
end
Loading
Loading
Loading
Loading
@@ -10,12 +10,16 @@ describe Projects::Serverless::FunctionsController do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes }
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
create(:cluster_kubernetes_namespace,
cluster: cluster,
cluster_project: cluster.cluster_project,
project: cluster.cluster_project.project)
project: cluster.cluster_project.project,
environment: environment)
end
 
before do
Loading
Loading
@@ -47,12 +51,11 @@ describe Projects::Serverless::FunctionsController do
end
 
context 'when cache is ready' do
let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
let(:knative_state) { true }
 
before do
allow_any_instance_of(Clusters::Cluster)
.to receive(:knative_services_finder)
allow(Clusters::KnativeServicesFinder)
.to receive(:new)
.and_return(knative_services_finder)
synchronous_reactive_cache(knative_services_finder)
stub_kubeclient_service_pods(
Loading
Loading
@@ -107,12 +110,12 @@ describe Projects::Serverless::FunctionsController do
context 'valid data', :use_clean_rails_memory_store_caching do
before do
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"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
},
*cluster.knative_services_finder(project).cache_args)
*knative_services_finder.cache_args)
end
 
it 'has a valid function name' do
Loading
Loading
@@ -140,12 +143,12 @@ describe Projects::Serverless::FunctionsController do
describe 'GET #index with data', :use_clean_rails_memory_store_caching do
before do
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"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
},
*cluster.knative_services_finder(project).cache_args)
*knative_services_finder.cache_args)
end
 
it 'has data' do
Loading
Loading
Loading
Loading
@@ -6,6 +6,7 @@ FactoryBot.define do
name 'test-cluster'
cluster_type :project_type
managed true
namespace_per_environment true
 
factory :cluster_for_group, traits: [:provided_by_gcp, :group]
 
Loading
Loading
@@ -29,6 +30,10 @@ FactoryBot.define do
end
end
 
trait :namespace_per_environment_disabled do
namespace_per_environment false
end
trait :provided_by_user do
provider_type :user
platform_type :kubernetes
Loading
Loading
Loading
Loading
@@ -5,12 +5,21 @@ FactoryBot.define do
association :cluster, :project, :provided_by_gcp
 
after(:build) do |kubernetes_namespace|
if kubernetes_namespace.cluster.project_type?
cluster_project = kubernetes_namespace.cluster.cluster_project
cluster = kubernetes_namespace.cluster
if cluster.project_type?
cluster_project = cluster.cluster_project
 
kubernetes_namespace.project = cluster_project.project
kubernetes_namespace.cluster_project = cluster_project
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
 
trait :with_token do
Loading
Loading
Loading
Loading
@@ -39,17 +39,19 @@ describe 'Functions', :js do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes }
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
create(:cluster_kubernetes_namespace,
cluster: cluster,
cluster_project: cluster.cluster_project,
project: cluster.cluster_project.project)
project: cluster.cluster_project.project,
environment: environment)
end
 
before do
allow_any_instance_of(Clusters::Cluster)
.to receive(:knative_services_finder)
allow(Clusters::KnativeServicesFinder)
.to receive(:new)
.and_return(knative_services_finder)
synchronous_reactive_cache(knative_services_finder)
stub_kubeclient_knative_services(stub_get_services_options)
Loading
Loading
Loading
Loading
@@ -7,15 +7,19 @@ describe Clusters::KnativeServicesFinder do
include ReactiveCachingHelpers
 
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(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) }
let(:namespace) do
create(:cluster_kubernetes_namespace,
cluster: cluster,
cluster_project: cluster.cluster_project,
project: project)
project: project,
environment: environment)
end
 
let(:finder) { described_class.new(cluster, environment) }
before do
stub_kubeclient_knative_services(namespace: namespace.namespace)
stub_kubeclient_service_pods(
Loading
Loading
@@ -35,7 +39,7 @@ describe Clusters::KnativeServicesFinder do
 
context 'when using synchronous reactive cache' do
before do
synchronous_reactive_cache(cluster.knative_services_finder(project))
synchronous_reactive_cache(finder)
end
 
context 'when there are functions for cluster namespace' do
Loading
Loading
@@ -60,21 +64,21 @@ describe Clusters::KnativeServicesFinder do
end
 
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'
end
 
describe '#services' do
subject { cluster.knative_services_finder(project).services }
subject { finder.services }
 
it_behaves_like 'a cached data'
end
 
describe '#knative_detected' do
subject { cluster.knative_services_finder(project).knative_detected }
subject { finder.knative_detected }
before do
synchronous_reactive_cache(cluster.knative_services_finder(project))
synchronous_reactive_cache(finder)
end
 
context 'when knative is installed' do
Loading
Loading
@@ -85,7 +89,7 @@ describe Clusters::KnativeServicesFinder do
it { is_expected.to be_truthy }
it "discovers knative installation" do
expect { subject }
.to change { cluster.kubeclient.knative_client.discovered }
.to change { finder.cluster.kubeclient.knative_client.discovered }
.from(false)
.to(true)
end
Loading
Loading
# 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
Loading
Loading
@@ -11,12 +11,15 @@ describe Projects::Serverless::FunctionsFinder do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes }
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
create(:cluster_kubernetes_namespace,
cluster: cluster,
cluster_project: cluster.cluster_project,
project: cluster.cluster_project.project)
project: project,
environment: environment)
end
 
before do
Loading
Loading
@@ -29,11 +32,9 @@ describe Projects::Serverless::FunctionsFinder do
end
 
context 'when reactive_caching has finished' do
let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
before do
allow_any_instance_of(Clusters::Cluster)
.to receive(:knative_services_finder)
allow(Clusters::KnativeServicesFinder)
.to receive(:new)
.and_return(knative_services_finder)
synchronous_reactive_cache(knative_services_finder)
end
Loading
Loading
@@ -47,8 +48,6 @@ describe Projects::Serverless::FunctionsFinder do
end
 
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
stub_kubeclient_knative_services(namespace: namespace.namespace)
stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
Loading
Loading
@@ -74,24 +73,24 @@ describe Projects::Serverless::FunctionsFinder do
 
it 'there are functions', :use_clean_rails_memory_store_caching do
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"],
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
end
 
it 'has a function', :use_clean_rails_memory_store_caching do
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"],
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)
expect(result).not_to be_empty
Loading
Loading
@@ -109,7 +108,7 @@ describe Projects::Serverless::FunctionsFinder do
let(:finder) { described_class.new(project) }
 
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'))
end
 
Loading
Loading
Loading
Loading
@@ -3,9 +3,9 @@
require 'spec_helper'
 
describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
let(:build) { create(:ci_build) }
describe '#unmet?' do
let(:build) { create(:ci_build) }
subject { described_class.new(build).unmet? }
 
context 'build has no deployment' do
Loading
Loading
@@ -18,7 +18,6 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
 
context 'build has a deployment' do
let!(:deployment) { create(:deployment, deployable: build, cluster: cluster) }
let(:cluster) { nil }
 
context 'and a cluster to deploy to' do
let(:cluster) { create(:cluster, :group) }
Loading
Loading
@@ -32,12 +31,17 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
end
 
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 }
 
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 }
end
Loading
Loading
@@ -45,34 +49,79 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
end
 
context 'and no cluster to deploy to' do
let(:cluster) { nil }
it { is_expected.to be_falsey }
end
end
end
 
describe '#complete!' do
let!(:deployment) { create(:deployment, deployable: build, cluster: cluster) }
let(:service) { double(execute: true) }
let(:cluster) { nil }
let(:build) { create(:ci_build) }
let(:prerequisite) { described_class.new(build) }
 
subject { described_class.new(build).complete! }
subject { prerequisite.complete! }
 
context 'completion is required' do
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
expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService)
.to receive(:new)
.with(cluster: cluster, kubernetes_namespace: instance_of(Clusters::KubernetesNamespace))
.and_return(service)
before do
allow(Clusters::KubernetesNamespaceFinder).to receive(:new)
.and_return(double(execute: nil))
end
 
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
 
context 'completion is not required' do
before do
allow(prerequisite).to receive(:unmet?).and_return(false)
end
it 'does not create a namespace' do
expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:new)
 
Loading
Loading
# 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
Loading
Loading
@@ -23,7 +23,7 @@ describe Gitlab::Prometheus::QueryVariables do
 
context 'with deployment platform' 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
create(:cluster, :project, :provided_by_user, projects: [project])
Loading
Loading
@@ -38,8 +38,8 @@ describe Gitlab::Prometheus::QueryVariables do
let(:project2) { create(:project) }
let(:kube_namespace) { k8s_ns.namespace }
 
let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project) }
let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2) }
let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project, environment: environment) }
let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2, environment: environment) }
 
before do
group.projects << project
Loading
Loading
Loading
Loading
@@ -38,11 +38,6 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
 
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
subject { described_class.enabled }
 
Loading
Loading
@@ -534,60 +529,39 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
 
describe '#find_or_initialize_kubernetes_namespace_for_project' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.projects.first }
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
describe '#kubernetes_namespace_for' do
let(:cluster) { create(:cluster, :group) }
let(:environment) { create(:environment) }
 
context 'with a service account token' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, project: project, cluster: cluster) }
subject { cluster.kubernetes_namespace_for(environment) }
 
it { is_expected.to eq kubernetes_namespace }
end
end
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
before do
expect(Clusters::KubernetesNamespaceFinder).to receive(:new)
.with(cluster, project: environment.project, environment_slug: environment.slug)
.and_return(double(execute: persisted_namespace))
end
 
context 'a custom scope is provided' do
let(:scope) { cluster.kubernetes_namespaces.has_service_account_token }
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) }
context 'a persisted namespace exists' do
let(:persisted_namespace) { create(:cluster_kubernetes_namespace) }
 
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
it { is_expected.to eq persisted_namespace.namespace }
end
 
context 'with a service account token' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, project: project, cluster: cluster) }
context 'no persisted namespace exists' do
let(:persisted_namespace) { nil }
let(:namespace_generator) { double }
let(:default_namespace) { 'a-default-namespace' }
 
it { is_expected.to eq kubernetes_namespace }
end
before do
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
it { is_expected.to eq default_namespace }
end
end
 
Loading
Loading
Loading
Loading
@@ -24,70 +24,60 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
end
end
 
describe 'namespace uniqueness validation' do
let(:cluster_project) { create(:cluster_project) }
let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, namespace: 'my-namespace') }
describe '.with_environment_slug' do
let(:cluster) { create(:cluster, :group) }
let(:environment) { create(:environment, slug: slug) }
 
subject { kubernetes_namespace }
let(:slug) { 'production' }
 
context 'when cluster is using the namespace' do
before do
create(:cluster_kubernetes_namespace,
cluster: kubernetes_namespace.cluster,
namespace: 'my-namespace')
end
subject { described_class.with_environment_slug(slug) }
 
it { is_expected.not_to be_valid }
end
context 'there is no associated environment' do
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_valid }
it { is_expected.to be_empty }
end
end
 
describe '#set_defaults' do
let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace) }
let(:cluster) { kubernetes_namespace.cluster }
let(:platform) { kubernetes_namespace.platform_kubernetes }
subject { kubernetes_namespace.set_defaults }
describe '#namespace' do
before do
platform.update_column(:namespace, namespace)
context 'there is an assicated environment' do
let!(:namespace) do
create(
:cluster_kubernetes_namespace,
cluster: cluster,
project: environment.project,
environment: environment
)
end
 
context 'when platform has a namespace assigned' do
let(:namespace) { 'platform-namespace' }
it 'copies the namespace' do
subject
expect(kubernetes_namespace.namespace).to eq('platform-namespace')
end
context 'with a matching slug' do
it { is_expected.to eq [namespace] }
end
 
context 'when platform does not have namespace assigned' do
let(:project) { kubernetes_namespace.project }
let(:namespace) { nil }
let(:project_slug) { "#{project.path}-#{project.id}" }
it 'fallbacks to project namespace' do
subject
context 'without a matching slug' do
let(:environment) { create(:environment, slug: 'staging') }
 
expect(kubernetes_namespace.namespace).to eq(project_slug)
end
it { is_expected.to be_empty }
end
end
end
 
describe '#service_account_name' do
let(:service_account_name) { "#{kubernetes_namespace.namespace}-service-account" }
describe 'namespace uniqueness validation' do
let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, namespace: 'my-namespace') }
 
it 'sets a service account name based on namespace' do
subject
subject { kubernetes_namespace }
 
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
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
 
Loading
Loading
Loading
Loading
@@ -205,192 +205,77 @@ describe Clusters::Platforms::Kubernetes do
it { is_expected.to be_truthy }
end
 
describe '#kubernetes_namespace_for' do
let(:cluster) { create(:cluster, :project) }
let(:project) { cluster.project }
let(:platform) do
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
describe '#predefined_variables' do
let(:project) { create(:project) }
let(:cluster) { create(:cluster, :group, platform_kubernetes: platform) }
let(:platform) { create(:cluster_platform_kubernetes) }
let(:persisted_namespace) { create(:cluster_kubernetes_namespace, project: project, cluster: cluster) }
 
it { is_expected.to eq("#{project.path}-#{project.id}") }
end
end
let(:environment_name) { 'env/production' }
let(:environment_slug) { Gitlab::Slug::Environment.new(environment_name).generate }
 
context 'when kubernetes namespace is not present' do
it { is_expected.to eq("#{project.path}-#{project.id}") }
end
end
end
subject { platform.predefined_variables(project: project, environment_name: environment_name) }
 
describe '#predefined_variables' do
let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem) }
let(:api_url) { 'https://kube.domain.com' }
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
before do
allow(Clusters::KubernetesNamespaceFinder).to receive(:new)
.with(cluster, project: project, environment_slug: environment_slug)
.and_return(double(execute: persisted_namespace))
end
 
context 'kubernetes namespace is created with no service account token' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster) }
it { is_expected.to include(key: 'KUBE_URL', value: platform.api_url, public: true) }
 
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
expect(subject).not_to include(
{ key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true }
)
end
it { is_expected.to include(key: 'KUBE_CA_PEM', value: ca_pem, public: true) }
it { is_expected.to include(key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true) }
end
 
context 'kubernetes namespace is created with service account token' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, cluster: cluster) }
it_behaves_like 'setting variables'
context 'kubernetes namespace exists' do
let(:variable) { Hash(key: :fake_key, value: 'fake_value') }
let(:namespace_variables) { Gitlab::Ci::Variables::Collection.new([variable]) }
 
it 'sets KUBE_TOKEN' do
expect(subject).to include(
{ key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true }
)
before do
expect(persisted_namespace).to receive(:predefined_variables).and_return(namespace_variables)
end
 
context 'the cluster has been set to unmanaged after the namespace was created' do
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
it { is_expected.to include(variable) }
end
 
context 'group level cluster' do
let!(:cluster) { create(:cluster, :group, platform_kubernetes: kubernetes) }
let(:project) { create(:project, group: cluster.group) }
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) }
context 'kubernetes namespace does not exist' do
let(:persisted_namespace) { nil }
let(:namespace) { 'kubernetes-namespace' }
let(:kubeconfig) { 'kubeconfig' }
 
it_behaves_like 'setting variables'
it 'sets KUBE_TOKEN' do
expect(subject).to include(
{ key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true }
)
end
end
before do
allow(Gitlab::Kubernetes::DefaultNamespace).to receive(:new)
.with(cluster, project: project).and_return(double(from_environment_name: namespace))
allow(platform).to receive(:kubeconfig).with(namespace).and_return(kubeconfig)
end
 
context 'kubernetes namespace exists for the project' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, cluster: cluster, project: project) }
it { is_expected.not_to include(key: 'KUBE_TOKEN', value: platform.token, public: false, masked: true) }
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
expect(subject).to include(
{ key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true }
)
end
it { is_expected.to include(key: 'KUBE_TOKEN', value: platform.token, public: false, masked: true) }
it { is_expected.to include(key: 'KUBE_NAMESPACE', value: namespace) }
it { is_expected.to include(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) }
end
end
 
context 'with a domain' do
let!(:cluster) do
create(:cluster, :provided_by_gcp, :with_domain,
platform_kubernetes: kubernetes)
end
context 'cluster variables' do
let(:variable) { Hash(key: :fake_key, value: 'fake_value') }
let(:cluster_variables) { Gitlab::Ci::Variables::Collection.new([variable]) }
 
it 'sets KUBE_INGRESS_BASE_DOMAIN' do
expect(subject).to include(
{ key: 'KUBE_INGRESS_BASE_DOMAIN', value: cluster.domain, public: true }
)
before do
expect(cluster).to receive(:predefined_variables).and_return(cluster_variables)
end
it { is_expected.to include(variable) }
end
end
 
Loading
Loading
@@ -410,7 +295,7 @@ describe Clusters::Platforms::Kubernetes do
end
 
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(:terminals) { kube_terminals(service, pod) }
let(:pods) { [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")] }
Loading
Loading
Loading
Loading
@@ -575,6 +575,34 @@ describe Environment, :use_clean_rails_memory_store_caching do
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
subject { environment.terminals }
 
Loading
Loading
@@ -823,4 +851,35 @@ describe Environment, :use_clean_rails_memory_store_caching do
subject.prometheus_adapter
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
Loading
Loading
@@ -2594,45 +2594,33 @@ describe Project do
end
 
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
expect(project.deployment_variables).to eq []
end
subject { project.deployment_variables(environment: environment) }
before do
expect(project).to receive(:deployment_platform).with(environment: environment)
.and_return(deployment_platform)
end
 
context 'when project uses mock deployment service' do
let(:project) { create(:mock_deployment_project) }
context 'when project has no deployment platform' do
let(:deployment_platform) { nil }
 
it 'returns an empty array' do
expect(project.deployment_variables).to eq []
end
it { is_expected.to eq [] }
end
 
context 'when project has a deployment service' do
context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has not been executed' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
context 'when project has a deployment platform' do
let(:platform_variables) { %w(platform variables) }
let(:deployment_platform) { double }
 
it 'does not return variables from this service' do
expect(project.deployment_variables).not_to include(
{ key: 'KUBE_TOKEN', value: project.deployment_platform.token, public: false, masked: true }
)
end
before do
expect(deployment_platform).to receive(:predefined_variables)
.with(project: project, environment_name: environment)
.and_return(platform_variables)
end
 
context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has been executed' do
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
it { is_expected.to eq platform_variables }
end
end
 
Loading
Loading
Loading
Loading
@@ -336,7 +336,6 @@ describe API::ProjectClusters do
it 'does not update cluster attributes' do
expect(cluster.domain).not_to eq('new_domain.com')
expect(cluster.platform_kubernetes.namespace).not_to eq('invalid_namespace')
expect(cluster.kubernetes_namespace_for(project)).not_to eq('invalid_namespace')
end
 
it 'returns validation errors' do
Loading
Loading
# 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
Loading
Loading
@@ -9,8 +9,9 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
let(:platform) { cluster.platform }
let(:api_url) { 'https://kubernetes.example.com' }
let(:project) { cluster.project }
let(:environment) { create(:environment, project: project) }
let(:cluster_project) { cluster.cluster_project }
let(:namespace) { "#{project.path}-#{project.id}" }
let(:namespace) { "#{project.name}-#{project.id}-#{environment.slug}" }
 
subject do
described_class.new(
Loading
Loading
@@ -79,7 +80,8 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
let(:kubernetes_namespace) do
build(:cluster_kubernetes_namespace,
cluster: cluster,
project: project)
project: project,
environment: environment)
end
 
it_behaves_like 'successful creation of kubernetes namespace'
Loading
Loading
@@ -92,20 +94,22 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
build(:cluster_kubernetes_namespace,
cluster: cluster,
project: cluster_project.project,
cluster_project: cluster_project)
cluster_project: cluster_project,
environment: environment)
end
 
it_behaves_like 'successful creation of kubernetes namespace'
end
 
context 'when there is a Kubernetes Namespace associated' do
let(:namespace) { 'new-namespace' }
let(:namespace) { "new-namespace-#{environment.slug}" }
 
let(:kubernetes_namespace) do
create(:cluster_kubernetes_namespace,
cluster: cluster,
project: cluster_project.project,
cluster_project: cluster_project)
cluster_project: cluster_project,
environment: environment)
end
 
before do
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment