Commit d1dd1bdc authored by Thong Kuah's avatar Thong Kuah

Merge branch '61156-instance-level-cluster-pod-terminal-access-ee' into 'master'

Instance-level cluster pod terminal access

See merge request gitlab-org/gitlab-ee!13307
parents eae82069 5b877268
...@@ -4,7 +4,6 @@ module Clusters ...@@ -4,7 +4,6 @@ module Clusters
module Platforms module Platforms
class Kubernetes < ApplicationRecord class Kubernetes < ApplicationRecord
include Gitlab::Kubernetes include Gitlab::Kubernetes
include ReactiveCaching
include EnumWithNil include EnumWithNil
include AfterCommitQueue include AfterCommitQueue
...@@ -46,8 +45,6 @@ module Clusters ...@@ -46,8 +45,6 @@ module Clusters
validate :prevent_modification, on: :update validate :prevent_modification, on: :update
after_save :clear_reactive_cache!
alias_attribute :ca_pem, :ca_cert alias_attribute :ca_pem, :ca_cert
delegate :enabled?, to: :cluster, allow_nil: true delegate :enabled?, to: :cluster, allow_nil: true
...@@ -96,27 +93,16 @@ module Clusters ...@@ -96,27 +93,16 @@ module Clusters
end end
end end
# Constructs a list of terminals from the reactive cache def calculate_reactive_cache_for(environment)
#
# Returns nil if the cache is empty, in which case you should try again a
# short time later
def terminals(environment)
with_reactive_cache do |data|
project = environment.project
pods = filter_by_project_environment(data[:pods], project.full_path_slug, environment.slug)
terminals = pods.flat_map { |pod| terminals_for_pod(api_url, cluster.kubernetes_namespace_for(project), pod) }.compact
terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end
end
# Caches resources in the namespace so other calls don't need to block on
# network access
def calculate_reactive_cache
return unless enabled? return unless enabled?
# We may want to cache extra things in the future { pods: read_pods(environment.deployment_namespace) }
{ pods: read_pods } end
def terminals(environment, data)
pods = filter_by_project_environment(data[:pods], environment.project.full_path_slug, environment.slug)
terminals = pods.flat_map { |pod| terminals_for_pod(api_url, environment.deployment_namespace, pod) }.compact
terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end end
def kubeclient def kubeclient
...@@ -133,6 +119,12 @@ module Clusters ...@@ -133,6 +119,12 @@ module Clusters
ca_pem: ca_pem) ca_pem: ca_pem)
end end
def read_pods(namespace)
kubeclient.get_pods(namespace: namespace).as_json
rescue Kubeclient::ResourceNotFoundError
[]
end
def build_kube_client! def build_kube_client!
raise "Incomplete settings" unless api_url raise "Incomplete settings" unless api_url
...@@ -148,19 +140,6 @@ module Clusters ...@@ -148,19 +140,6 @@ module Clusters
) )
end end
# Returns a hash of all pods in the namespace
def read_pods
# TODO: The project lookup here should be moved (to environment?),
# which will enable reading pods from the correct namespace for group
# and instance clusters.
# This will be done in https://gitlab.com/gitlab-org/gitlab-ce/issues/61156
return [] unless cluster.project_type?
kubeclient.get_pods(namespace: cluster.kubernetes_namespace_for(cluster.first_project)).as_json
rescue Kubeclient::ResourceNotFoundError
[]
end
def kubeclient_ssl_options def kubeclient_ssl_options
opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
class Environment < ApplicationRecord class Environment < ApplicationRecord
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include ReactiveCaching
# Used to generate random suffixes for the slug # Used to generate random suffixes for the slug
LETTERS = ('a'..'z').freeze LETTERS = ('a'..'z').freeze
NUMBERS = ('0'..'9').freeze NUMBERS = ('0'..'9').freeze
...@@ -17,6 +19,7 @@ class Environment < ApplicationRecord ...@@ -17,6 +19,7 @@ class Environment < ApplicationRecord
before_validation :generate_slug, if: ->(env) { env.slug.blank? } before_validation :generate_slug, if: ->(env) { env.slug.blank? }
before_save :set_environment_type before_save :set_environment_type
after_save :clear_reactive_cache!
validates :name, validates :name,
presence: true, presence: true,
...@@ -159,7 +162,21 @@ class Environment < ApplicationRecord ...@@ -159,7 +162,21 @@ class Environment < ApplicationRecord
end end
def terminals def terminals
deployment_platform.terminals(self) if has_terminals? with_reactive_cache do |data|
deployment_platform.terminals(self, data)
end
end
def calculate_reactive_cache
return unless has_terminals? && !project.pending_delete?
deployment_platform.calculate_reactive_cache_for(self)
end
def deployment_namespace
strong_memoize(:kubernetes_namespace) do
deployment_platform&.kubernetes_namespace_for(project)
end
end end
def has_metrics? def has_metrics?
......
---
title: Enable terminals for instance and group clusters
merge_request: 28613
author:
type: added
...@@ -138,14 +138,6 @@ The result will then be: ...@@ -138,14 +138,6 @@ The result will then be:
- The Staging cluster will be used for the `deploy to staging` job. - The Staging cluster will be used for the `deploy to staging` job.
- The Production cluster will be used for the `deploy to production` job. - The Production cluster will be used for the `deploy to production` job.
## Unavailable features
The following features are not currently available for group-level clusters:
1. Terminals (see [related issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/55487)).
1. Pod logs (see [related issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/55488)).
1. Deployment boards (see [related issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/55489)).
<!-- ## Troubleshooting <!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues Include any troubleshooting steps that you can foresee. If you know beforehand what issues
......
...@@ -33,7 +33,7 @@ module EE ...@@ -33,7 +33,7 @@ module EE
end end
def pod_logs def pod_logs
environment.deployment_platform.read_pod_logs(params[:pod_name]) environment.deployment_platform.read_pod_logs(params[:pod_name], environment.deployment_namespace)
end end
def authorize_create_environment_terminal! def authorize_create_environment_terminal!
......
...@@ -6,71 +6,36 @@ module EE ...@@ -6,71 +6,36 @@ module EE
LOGS_LIMIT = 500.freeze LOGS_LIMIT = 500.freeze
def rollout_status(environment) def calculate_reactive_cache_for(environment)
result = with_reactive_cache do |data|
project = environment.project
deployments = filter_by_project_environment(data[:deployments], project.full_path_slug, environment.slug)
pods = filter_by_project_environment(data[:pods], project.full_path_slug, environment.slug) if data[:pods]&.any?
legacy_deployments = filter_by_legacy_label(data[:deployments], project.full_path_slug, environment.slug)
::Gitlab::Kubernetes::RolloutStatus.from_deployments(*deployments, pods: pods, legacy_deployments: legacy_deployments)
end
result || ::Gitlab::Kubernetes::RolloutStatus.loading
end
def calculate_reactive_cache
result = super result = super
result[:deployments] = read_deployments if result result[:deployments] = read_deployments(environment.deployment_namespace) if result
result result
end end
def reactive_cache_updated def rollout_status(environment, data)
super project = environment.project
if first_project
::Gitlab::EtagCaching::Store.new.tap do |store|
store.touch(
::Gitlab::Routing.url_helpers.project_environments_path(first_project, format: :json))
end
end
end
def read_deployments deployments = filter_by_project_environment(data[:deployments], project.full_path_slug, environment.slug)
return [] unless first_project pods = filter_by_project_environment(data[:pods], project.full_path_slug, environment.slug) if data[:pods]&.any?
kubeclient.get_deployments(namespace: kubernetes_namespace_for(first_project)).as_json legacy_deployments = filter_by_legacy_label(data[:deployments], project.full_path_slug, environment.slug)
rescue KubeException => err
raise err unless err.error_code == 404
[] ::Gitlab::Kubernetes::RolloutStatus.from_deployments(*deployments, pods: pods, legacy_deployments: legacy_deployments)
end end
def read_pod_logs(pod_name, container: nil) def read_pod_logs(pod_name, namespace, container: nil)
return [] unless first_project kubeclient.get_pod_log(pod_name, namespace, container: container, tail_lines: LOGS_LIMIT).as_json
rescue Kubeclient::ResourceNotFoundError
kubeclient.get_pod_log(pod_name, kubernetes_namespace_for(first_project), container: container, tail_lines: LOGS_LIMIT).as_json
rescue ::Kubeclient::HttpError => err
raise err unless err.error_code == 404
[] []
end end
private private
## def read_deployments(namespace)
# TODO: KubernetesService is soon to be removed (https://gitlab.com/gitlab-org/gitlab-ce/issues/39217), kubeclient.get_deployments(namespace: namespace).as_json
# after which we can retrieve the project from the cluster in all cases. rescue Kubeclient::ResourceNotFoundError
# []
# This currently only works for project-level clusters, this is likely to be fixed as part of
# https://gitlab.com/gitlab-org/gitlab-ce/issues/61156, which will require logic to select
# a project from a cluster based on an environment.
def first_project
return project unless respond_to?(:cluster)
cluster.first_project if cluster.project_type?
end end
end end
end end
...@@ -12,6 +12,15 @@ module EE ...@@ -12,6 +12,15 @@ module EE
has_one :last_pipeline, through: :last_deployable, source: 'pipeline' has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
end end
def reactive_cache_updated
super
::Gitlab::EtagCaching::Store.new.tap do |store|
store.touch(
::Gitlab::Routing.url_helpers.project_environments_path(project, format: :json))
end
end
def pod_names def pod_names
return [] unless rollout_status return [] unless rollout_status
...@@ -37,7 +46,11 @@ module EE ...@@ -37,7 +46,11 @@ module EE
end end
def rollout_status def rollout_status
deployment_platform.rollout_status(self) if has_terminals? result = with_reactive_cache do |data|
deployment_platform.rollout_status(self, data)
end
result || ::Gitlab::Kubernetes::RolloutStatus.loading
end end
end end
end end
---
title: Enable deployment boards and pod logs for instance and group clusters
merge_request: 13307
author:
type: added
...@@ -84,8 +84,10 @@ describe Projects::EnvironmentsController do ...@@ -84,8 +84,10 @@ describe Projects::EnvironmentsController do
environment_scope: '*', projects: [project]) environment_scope: '*', projects: [project])
create(:deployment, :success, environment: environment) create(:deployment, :success, environment: environment)
allow_any_instance_of(EE::KubernetesService).to receive(:read_pod_logs).with(pod_name).and_return(kube_logs_body) allow_any_instance_of(EE::KubernetesService).to receive(:read_pod_logs)
allow_any_instance_of(Gitlab::Kubernetes::RolloutStatus).to receive(:instances).and_return([{ pod_name: pod_name }]) .with(pod_name, environment.deployment_namespace).and_return(kube_logs_body)
allow_any_instance_of(Gitlab::Kubernetes::RolloutStatus).to receive(:instances)
.and_return([{ pod_name: pod_name }])
end end
context 'when unlicensed' do context 'when unlicensed' do
......
...@@ -16,7 +16,8 @@ describe 'Environment > Pod Logs', :js do ...@@ -16,7 +16,8 @@ describe 'Environment > Pod Logs', :js do
create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project]) create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project])
create(:deployment, :success, environment: environment) create(:deployment, :success, environment: environment)
allow_any_instance_of(EE::KubernetesService).to receive(:read_pod_logs).with(pod_name).and_return(kube_logs_body) allow_any_instance_of(EE::KubernetesService).to receive(:read_pod_logs)
.with(pod_name, environment.deployment_namespace).and_return(kube_logs_body)
allow_any_instance_of(EE::Environment).to receive(:pod_names).and_return(pod_names) allow_any_instance_of(EE::Environment).to receive(:pod_names).and_return(pod_names)
sign_in(project.owner) sign_in(project.owner)
......
require 'spec_helper' require 'spec_helper'
describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching do describe Clusters::Platforms::Kubernetes do
include KubernetesHelpers include KubernetesHelpers
include ReactiveCachingHelpers
describe '#calculate_reactive_cache' do describe '#rollout_status' do
subject { service.calculate_reactive_cache } let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:environment) { create(:environment) }
let(:cache_data) { Hash(deployments: deployments, pods: pods) }
let(:pods) { [kube_pod] }
let(:deployments) { [kube_deployment] }
let(:legacy_deployments) { [kube_deployment] }
subject { service.rollout_status(environment, cache_data) }
before do
allow(service).to receive(:filter_by_project_environment).with(pods, any_args).and_return(pods)
allow(service).to receive(:filter_by_project_environment).with(deployments, any_args).and_return(deployments)
allow(service).to receive(:filter_by_legacy_label).with(deployments, any_args).and_return(legacy_deployments)
end
it 'requests the rollout status' do
expect(::Gitlab::Kubernetes::RolloutStatus).to receive(:from_deployments).with(*deployments, pods: pods, legacy_deployments: legacy_deployments)
subject
end
context 'no pod data provided' do
let(:pods) { [] }
it 'requests the rollout status without pod information' do
expect(::Gitlab::Kubernetes::RolloutStatus).to receive(:from_deployments).with(*deployments, pods: nil, legacy_deployments: legacy_deployments)
let(:cluster) { create(:cluster, :project, enabled: true, platform_kubernetes: service) } subject
end
end
end
describe '#read_pod_logs' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
let(:service) { create(:cluster_platform_kubernetes, :configured) } let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:namespace) { cluster.kubernetes_namespace_for(cluster.first_project) } let(:pod_name) { 'pod-1' }
let(:namespace) { 'app' }
subject { service.read_pod_logs(pod_name, namespace) }
context 'when kubernetes responds with valid pods and deployments' do context 'when kubernetes responds with valid logs' do
before do before do
stub_kubeclient_pods(namespace) stub_kubeclient_logs(pod_name, namespace)
stub_kubeclient_deployments(namespace)
end end
it { is_expected.to eq(pods: [kube_pod], deployments: [kube_deployment]) } shared_examples 'successful log request' do
it { expect(subject.body).to eq("\"Log 1\\nLog 2\\nLog 3\"") }
end
context 'on a project level cluster' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
include_examples 'successful log request'
end
context 'on a cluster that is not project level' do context 'on a group level cluster' do
let(:cluster) { create(:cluster, :group, platform_kubernetes: service) } let(:cluster) { create(:cluster, :group, platform_kubernetes: service) }
it { is_expected.to eq(pods: [], deployments: []) } include_examples 'successful log request'
end
context 'on an instance level cluster' do
let(:cluster) { create(:cluster, :instance, platform_kubernetes: service) }
include_examples 'successful log request'
end end
end end
context 'when kubernetes responds with 500s' do
before do
stub_kubeclient_logs(pod_name, namespace, status: 500)
end
it { expect { subject }.to raise_error(::Kubeclient::HttpError) }
end
context 'when kubernetes responds with 404s' do context 'when kubernetes responds with 404s' do
before do before do
stub_kubeclient_pods(namespace, status: 404) stub_kubeclient_logs(pod_name, namespace, status: 404)
stub_kubeclient_deployments(namespace, status: 404)
end end
it { is_expected.to eq(pods: [], deployments: []) } it { is_expected.to be_empty }
end end
end end
describe '#read_pod_logs' do describe '#calculate_reactive_cache_for' do
subject { service.read_pod_logs(pod_name) } let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
let(:pod_name) { 'foo' }
let(:cluster) { create(:cluster, :project, enabled: true, platform_kubernetes: service) }
let(:service) { create(:cluster_platform_kubernetes, :configured) } let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:namespace) { cluster.kubernetes_namespace_for(cluster.first_project) } let(:namespace) { 'app' }
let(:environment) { instance_double(Environment, deployment_namespace: namespace) }
context 'when kubernetes responds with valid logs' do subject { service.calculate_reactive_cache_for(environment) }
before do
allow(service).to receive(:read_pods).and_return([])
end
context 'when kubernetes responds with valid deployments' do
before do before do
stub_kubeclient_logs(pod_name, namespace) stub_kubeclient_deployments(namespace)
end
shared_examples 'successful deployment request' do
it { is_expected.to include(deployments: [kube_deployment]) }
end end
it 'returns logs' do context 'on a project level cluster' do
expect(subject.body).to eq("\"Log 1\\nLog 2\\nLog 3\"") let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
include_examples 'successful deployment request'
end end
context 'on a cluster that is not project level' do context 'on a group level cluster' do
let(:cluster) { create(:cluster, :group, platform_kubernetes: service) } let(:cluster) { create(:cluster, :group, platform_kubernetes: service) }
it { is_expected.to be_empty } include_examples 'successful deployment request'
end
context 'on an instance level cluster' do
let(:cluster) { create(:cluster, :instance, platform_kubernetes: service) }
include_examples 'successful deployment request'
end end
end end
context 'when kubernetes response with 500s' do context 'when kubernetes responds with 500s' do
before do before do
stub_kubeclient_logs(pod_name, namespace, status: 500) stub_kubeclient_deployments(namespace, status: 500)
end end
it { expect { subject }.to raise_error(::Kubeclient::HttpError) } it { expect { subject }.to raise_error(::Kubeclient::HttpError) }
...@@ -70,10 +139,10 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -70,10 +139,10 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
context 'when kubernetes responds with 404s' do context 'when kubernetes responds with 404s' do
before do before do
stub_kubeclient_logs(pod_name, namespace, status: 404) stub_kubeclient_deployments(namespace, status: 404)
end end
it { is_expected.to be_empty } it { is_expected.to include(deployments: []) }
end end
end end
end end
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
require 'spec_helper' require 'spec_helper'
describe Environment do describe Environment, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
let(:project) { create(:project, :stubbed_repository) } let(:project) { create(:project, :stubbed_repository) }
let(:environment) { create(:environment, project: project) } let(:environment) { create(:environment, project: project) }
...@@ -14,13 +16,16 @@ describe Environment do ...@@ -14,13 +16,16 @@ describe Environment do
end end
context 'when environment has a rollout status' do context 'when environment has a rollout status' do
it 'returns the pod_names' do let(:pod_name) { 'pod_1' }
pod_name = "pod_1" let(:rollout_status) { instance_double(::Gitlab::Kubernetes::RolloutStatus, instances: [{ pod_name: pod_name }]) }
before do
create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project]) create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project])
create(:deployment, :success, environment: environment) create(:deployment, :success, environment: environment)
end
allow_any_instance_of(Gitlab::Kubernetes::RolloutStatus).to receive(:instances) it 'returns the pod_names' do
.and_return([{ pod_name: pod_name }]) allow(environment).to receive(:rollout_status).and_return(rollout_status)
expect(environment.pod_names).to eq([pod_name]) expect(environment.pod_names).to eq([pod_name])
end end
...@@ -98,44 +103,54 @@ describe Environment do ...@@ -98,44 +103,54 @@ describe Environment do
end end
end end
describe '#reactive_cache_updated' do
let(:mock_store) { double }
subject { environment.reactive_cache_updated }
it 'expires the environments path for the project' do
expect(::Gitlab::EtagCaching::Store).to receive(:new).and_return(mock_store)
expect(mock_store).to receive(:touch).with(::Gitlab::Routing.url_helpers.project_environments_path(project, format: :json))
subject
end
end
describe '#rollout_status' do describe '#rollout_status' do
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do let(:cluster) { create(:cluster, :project, :provided_by_user) }
subject { environment.rollout_status } let(:project) { cluster.project }
let(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, :success, environment: environment) }
context 'when the environment has rollout status' do subject { environment.rollout_status }
before do
allow(environment).to receive(:has_terminals?).and_return(true)
end
it 'returns the rollout status from the deployment service' do context 'cached rollout status is present' do
expect(environment.deployment_platform) let(:pods) { %w(pod1 pod2) }
.to receive(:rollout_status).with(environment) let(:deployments) { %w(deployment1 deployment2) }
.and_return(:fake_rollout_status)
is_expected.to eq(:fake_rollout_status) before do
end stub_reactive_cache(environment, pods: pods, deployments: deployments)
end end
context 'when the environment does not have rollout status' do it 'fetches the rollout status from the deployment platform' do
before do expect(environment.deployment_platform).to receive(:rollout_status)
allow(environment).to receive(:has_terminals?).and_return(false) .with(environment, pods: pods, deployments: deployments)
end .and_return(:mock_rollout_status)
it { is_expected.to eq(nil) } is_expected.to eq(:mock_rollout_status)
end end
end end
context 'when user configured kubernetes from Integration > Kubernetes' do context 'cached rollout status is not present' do
let(:project) { create(:kubernetes_project) } before do
stub_reactive_cache(environment, nil)
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' end
end
context 'when user configured kubernetes from CI/CD > Clusters' do it 'falls back to a loading status' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } expect(::Gitlab::Kubernetes::RolloutStatus).to receive(:loading).and_return(:mock_loading_status)
let(:project) { cluster.project }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' is_expected.to eq(:mock_loading_status)
end
end end
end end
end end
require 'spec_helper'
describe KubernetesService, models: true, use_clean_rails_memory_store_caching: true do
include KubernetesHelpers
include ReactiveCachingHelpers
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
let(:service) { create(:kubernetes_service) }
describe '#rollout_status' do
let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
subject(:rollout_status) { service.rollout_status(environment) }
context 'legacy deployments based on app label' do
let(:legacy_deployment) do
kube_deployment(name: 'legacy-deployment').tap do |deployment|
deployment['metadata']['annotations'].delete('app.gitlab.com/env')
deployment['metadata']['annotations'].delete('app.gitlab.com/app')
deployment['metadata']['labels']['app'] = environment.slug
end
end
let(:legacy_pod) do
kube_pod(name: 'legacy-pod').tap do |pod|
pod['metadata']['annotations'].delete('app.gitlab.com/env')
pod['metadata']['annotations'].delete('app.gitlab.com/app')
pod['metadata']['labels']['app'] = environment.slug
end
end
context 'only legacy deployments' do
before do
stub_reactive_cache(
service,
deployments: [legacy_deployment],
pods: [legacy_pod]
)
end
it 'contains nothing' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status.deployments).to eq([])
end
it 'has the has_legacy_app_label flag' do
expect(rollout_status).to be_has_legacy_app_label
end
end
context 'new deployment based on annotations' do
let(:matched_deployment) { kube_deployment(name: 'matched-deployment', environment_slug: environment.slug, project_slug: project.full_path_slug) }
let(:matched_pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug) }
before do
stub_reactive_cache(
service,
deployments: [matched_deployment, legacy_deployment],
pods: [matched_pod, legacy_pod]
)
end
it 'contains only matching deployments' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status.deployments.map(&:name)).to contain_exactly('matched-deployment')
end
it 'does have the has_legacy_app_label flag' do
expect(rollout_status).to be_has_legacy_app_label
end
end
context 'deployment with app label not matching the environment' do
let(:other_deployment) do
kube_deployment(name: 'other-deployment').tap do |deployment|
deployment['metadata']['annotations'].delete('app.gitlab.com/env')
deployment['metadata']['annotations'].delete('app.gitlab.com/app')
deployment['metadata']['labels']['app'] = 'helm-app-label'
end
end
let(:other_pod) do
kube_pod(name: 'other-pod').tap do |pod|
pod['metadata']['annotations'].delete('app.gitlab.com/env')
pod['metadata']['annotations'].delete('app.gitlab.com/app')
pod['metadata']['labels']['app'] = environment.slug
end
end
before do
stub_reactive_cache(
service,
deployments: [other_deployment],
pods: [other_pod]
)
end
it 'does not have the has_legacy_app_label flag' do
expect(rollout_status).not_to be_has_legacy_app_label
end
end
end
context 'with valid deployments' do
let(:matched_deployment) { kube_deployment(environment_slug: environment.slug, project_slug: project.full_path_slug) }
let(:unmatched_deployment) { kube_deployment }
let(:matched_pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug) }
let(:unmatched_pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: 'Pending') }
before do
stub_reactive_cache(
service,
deployments: [matched_deployment, unmatched_deployment],
pods: [matched_pod, unmatched_pod]
)
end
it 'creates a matching RolloutStatus' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status.deployments.map(&:annotations)).to eq([
{ 'app.gitlab.com/app' => project.full_path_slug, 'app.gitlab.com/env' => 'env-000000' }
])
end
end
context 'with empty list of deployments' do
before do
stub_reactive_cache(
service,
deployments: [],
pods: []
)
end
it 'creates a matching RolloutStatus' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status).to be_not_found
end
end
context 'not yet loaded deployments' do
before do
stub_reactive_cache
end
it 'creates a matching RolloutStatus' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status).to be_loading
end
end
end
end
context 'when user configured kubernetes from Integration > Kubernetes' do
let(:project) { create(:kubernetes_project) }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
context 'when user configured kubernetes from CI/CD > Clusters' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
describe '#calculate_reactive_cache' do
let(:project) { create(:kubernetes_project) }
let(:service) { create(:kubernetes_service, project: project) }
let(:namespace) { service.kubernetes_namespace_for(project) }
subject { service.calculate_reactive_cache }
context 'when service is inactive' do
before do
service.active = false
end
it { is_expected.to be_nil }
end
context 'when kubernetes responds with valid pods and deployments' do
before do
stub_kubeclient_pods(namespace)
stub_kubeclient_deployments(namespace)
end
it { is_expected.to eq(pods: [kube_pod], deployments: [kube_deployment]) }
end
context 'when kubernetes responds with 500s' do
before do
stub_kubeclient_pods(namespace, status: 500)
stub_kubeclient_deployments(namespace, status: 500)
end
it { expect { subject }.to raise_error(Kubeclient::HttpError) }
end
context 'when kubernetes responds with 404s' do
before do
stub_kubeclient_pods(namespace, status: 404)
stub_kubeclient_deployments(namespace, status: 404)
end
it { is_expected.to eq(pods: [], deployments: []) }
end
end
describe '#reactive_cache_updated' do
subject { service.reactive_cache_updated }
shared_examples 'cache expiry' do
let(:mock_store) { double }
it 'expires the environments path for the project' do
expect(::Gitlab::EtagCaching::Store).to receive(:new).and_return(mock_store)
expect(mock_store).to receive(:touch).with(::Gitlab::Routing.url_helpers.project_environments_path(project, format: :json))
subject
end
end
context 'Platforms::Kubernetes' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { create(:kubernetes_service, project: project) }
let(:project) { cluster.first_project }
include_examples 'cache expiry'
end
context 'KubernetesService' do
let(:project) { create(:kubernetes_project) }
let(:service) { create(:kubernetes_service, project: project) }
include_examples 'cache expiry'
end
end
end
...@@ -2,13 +2,11 @@ ...@@ -2,13 +2,11 @@
require 'spec_helper' require 'spec_helper'
describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching do describe Clusters::Platforms::Kubernetes do
include KubernetesHelpers include KubernetesHelpers
include ReactiveCachingHelpers
it { is_expected.to belong_to(:cluster) } it { is_expected.to belong_to(:cluster) }
it { is_expected.to be_kind_of(Gitlab::Kubernetes) } it { is_expected.to be_kind_of(Gitlab::Kubernetes) }
it { is_expected.to be_kind_of(ReactiveCaching) }
it { is_expected.to respond_to :ca_pem } it { is_expected.to respond_to :ca_pem }
it { is_expected.to validate_exclusion_of(:namespace).in_array(%w(gitlab-managed-apps)) } it { is_expected.to validate_exclusion_of(:namespace).in_array(%w(gitlab-managed-apps)) }
...@@ -397,17 +395,16 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -397,17 +395,16 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end end
describe '#terminals' do describe '#terminals' do
subject { service.terminals(environment) } subject { service.terminals(environment, pods: pods) }
let!(:cluster) { create(:cluster, :project, platform_kubernetes: service) } let!(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
let(:project) { cluster.project } let(:project) { cluster.project }
let(:service) { create(:cluster_platform_kubernetes, :configured) } let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
let(:pods) { [{ "bad" => "pod" }] }
context 'with invalid pods' do context 'with invalid pods' do
it 'returns no terminals' do it 'returns no terminals' do
stub_reactive_cache(service, pods: [{ "bad" => "pod" }])
is_expected.to be_empty is_expected.to be_empty
end end
end end
...@@ -416,13 +413,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -416,13 +413,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
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(project), 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")] }
before do
stub_reactive_cache(
service,
pods: [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")]
)
end
it 'returns terminals' do it 'returns terminals' do
is_expected.to eq(terminals + terminals) is_expected.to eq(terminals + terminals)
...@@ -437,16 +428,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -437,16 +428,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end end
end end
describe '#calculate_reactive_cache' do describe '#calculate_reactive_cache_for' do
subject { service.calculate_reactive_cache }
let!(:cluster) { create(:cluster, :project, enabled: enabled, platform_kubernetes: service) }
let(:service) { create(:cluster_platform_kubernetes, :configured) } let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:enabled) { true } let(:pod) { kube_pod }
let(:namespace) { cluster.kubernetes_namespace_for(cluster.project) } let(:namespace) { pod["metadata"]["namespace"] }
let(:environment) { instance_double(Environment, deployment_namespace: namespace) }
context 'when cluster is disabled' do subject { service.calculate_reactive_cache_for(environment) }
let(:enabled) { false }
context 'when the kubernetes integration is disabled' do
before do
allow(service).to receive(:enabled?).and_return(false)
end
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
...@@ -457,7 +450,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -457,7 +450,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
stub_kubeclient_deployments(namespace) stub_kubeclient_deployments(namespace)
end end
it { is_expected.to include(pods: [kube_pod]) } it { is_expected.to include(pods: [pod]) }
end end
context 'when kubernetes responds with 500s' do context 'when kubernetes responds with 500s' do
...@@ -477,11 +470,5 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -477,11 +470,5 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to include(pods: []) } it { is_expected.to include(pods: []) }
end end
context 'when the cluster is not project level' do
let(:cluster) { create(:cluster, :group, platform_kubernetes: service) }
it { is_expected.to include(pods: []) }
end
end end
end end
...@@ -2,10 +2,14 @@ ...@@ -2,10 +2,14 @@
require 'spec_helper' require 'spec_helper'
describe Environment do describe Environment, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
let(:project) { create(:project, :stubbed_repository) } let(:project) { create(:project, :stubbed_repository) }
subject(:environment) { create(:environment, project: project) } subject(:environment) { create(:environment, project: project) }
it { is_expected.to be_kind_of(ReactiveCaching) }
it { is_expected.to belong_to(:project).required } it { is_expected.to belong_to(:project).required }
it { is_expected.to have_many(:deployments) } it { is_expected.to have_many(:deployments) }
...@@ -573,32 +577,65 @@ describe Environment do ...@@ -573,32 +577,65 @@ describe Environment do
describe '#terminals' do describe '#terminals' do
subject { environment.terminals } subject { environment.terminals }
context 'when the environment has terminals' do before do
allow(environment).to receive(:deployment_platform).and_return(double)
end
context 'reactive cache is empty' do
before do before do
allow(environment).to receive(:has_terminals?).and_return(true) stub_reactive_cache(environment, nil)
end end
context 'when user configured kubernetes from CI/CD > Clusters' do it { is_expected.to be_nil }
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } end
let(:project) { cluster.project }
context 'reactive cache has pod data' do
let(:cache_data) { Hash(pods: %w(pod1 pod2)) }
before do
stub_reactive_cache(environment, cache_data)
end
it 'returns the terminals from the deployment service' do it 'retrieves terminals from the deployment platform' do
expect(environment.deployment_platform) expect(environment.deployment_platform)
.to receive(:terminals).with(environment) .to receive(:terminals).with(environment, cache_data)
.and_return(:fake_terminals) .and_return(:fake_terminals)
is_expected.to eq(:fake_terminals) is_expected.to eq(:fake_terminals)
end
end end
end end
end
describe '#calculate_reactive_cache' do
let(:cluster) { create(:cluster, :project, :provided_by_user) }
let(:project) { cluster.project }
let(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, :success, environment: environment) }
subject { environment.calculate_reactive_cache }
it 'returns cache data from the deployment platform' do
expect(environment.deployment_platform).to receive(:calculate_reactive_cache_for)
.with(environment).and_return(pods: %w(pod1 pod2))
is_expected.to eq(pods: %w(pod1 pod2))
end
context 'when the environment does not have terminals' do context 'environment does not have terminals available' do
before do before do
allow(environment).to receive(:has_terminals?).and_return(false) allow(environment).to receive(:has_terminals?).and_return(false)
end end
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
context 'project is pending deletion' do
before do
allow(environment.project).to receive(:pending_delete?).and_return(true)
end
it { is_expected.to be_nil }
end
end end
describe '#has_metrics?' do describe '#has_metrics?' do
......
...@@ -3,17 +3,16 @@ ...@@ -3,17 +3,16 @@
require 'spec_helper' require 'spec_helper'
describe ReactiveCachingWorker do describe ReactiveCachingWorker do
let(:service) { project.deployment_platform }
describe '#perform' do describe '#perform' do
context 'when user configured kubernetes from CI/CD > Clusters' do context 'when user configured kubernetes from CI/CD > Clusters' 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, project: project) }
it 'calls #exclusively_update_reactive_cache!' do it 'calls #exclusively_update_reactive_cache!' do
expect_any_instance_of(Clusters::Platforms::Kubernetes).to receive(:exclusively_update_reactive_cache!) expect_any_instance_of(Environment).to receive(:exclusively_update_reactive_cache!)
described_class.new.perform("Clusters::Platforms::Kubernetes", service.id) described_class.new.perform("Environment", environment.id)
end 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