Commit 5b877268 authored by Tiger's avatar Tiger

EE - Move deployments and pod logs to Environment

This enables rollout statuses, deployment boards and pod logs
for group and project level clusters. Previously there was
no way to determine which project (and therefore kubernetes
namespace) to connect to, moving this logic onto Environment
means the assoicated project can be used to look up the
correct namespace.
parent f040a4ab
......@@ -33,7 +33,7 @@ module EE
end
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
def authorize_create_environment_terminal!
......
......@@ -6,71 +6,36 @@ module EE
LOGS_LIMIT = 500.freeze
def rollout_status(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
def calculate_reactive_cache_for(environment)
result = super
result[:deployments] = read_deployments if result
result[:deployments] = read_deployments(environment.deployment_namespace) if result
result
end
def reactive_cache_updated
super
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 rollout_status(environment, data)
project = environment.project
def read_deployments
return [] unless first_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?
kubeclient.get_deployments(namespace: kubernetes_namespace_for(first_project)).as_json
rescue KubeException => err
raise err unless err.error_code == 404
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
def read_pod_logs(pod_name, container: nil)
return [] unless first_project
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
def read_pod_logs(pod_name, namespace, container: nil)
kubeclient.get_pod_log(pod_name, namespace, container: container, tail_lines: LOGS_LIMIT).as_json
rescue Kubeclient::ResourceNotFoundError
[]
end
private
##
# TODO: KubernetesService is soon to be removed (https://gitlab.com/gitlab-org/gitlab-ce/issues/39217),
# after which we can retrieve the project from the cluster in all cases.
#
# 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?
def read_deployments(namespace)
kubeclient.get_deployments(namespace: namespace).as_json
rescue Kubeclient::ResourceNotFoundError
[]
end
end
end
......@@ -12,6 +12,15 @@ module EE
has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
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
return [] unless rollout_status
......@@ -37,7 +46,11 @@ module EE
end
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
---
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
environment_scope: '*', projects: [project])
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(Gitlab::Kubernetes::RolloutStatus).to receive(:instances).and_return([{ pod_name: pod_name }])
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(Gitlab::Kubernetes::RolloutStatus).to receive(:instances)
.and_return([{ pod_name: pod_name }])
end
context 'when unlicensed' do
......
......@@ -16,7 +16,8 @@ describe 'Environment > Pod Logs', :js do
create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project])
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)
sign_in(project.owner)
......
require 'spec_helper'
describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching do
describe Clusters::Platforms::Kubernetes do
include KubernetesHelpers
include ReactiveCachingHelpers
describe '#calculate_reactive_cache' do
subject { service.calculate_reactive_cache }
describe '#rollout_status' do
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(: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
stub_kubeclient_pods(namespace)
stub_kubeclient_deployments(namespace)
stub_kubeclient_logs(pod_name, namespace)
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) }
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
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
before do
stub_kubeclient_pods(namespace, status: 404)
stub_kubeclient_deployments(namespace, status: 404)
stub_kubeclient_logs(pod_name, namespace, status: 404)
end
it { is_expected.to eq(pods: [], deployments: []) }
it { is_expected.to be_empty }
end
end
describe '#read_pod_logs' do
subject { service.read_pod_logs(pod_name) }
let(:pod_name) { 'foo' }
let(:cluster) { create(:cluster, :project, enabled: true, platform_kubernetes: service) }
describe '#calculate_reactive_cache_for' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
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
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
it 'returns logs' do
expect(subject.body).to eq("\"Log 1\\nLog 2\\nLog 3\"")
context 'on a project level cluster' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
include_examples 'successful deployment 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) }
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
context 'when kubernetes response with 500s' do
context 'when kubernetes responds with 500s' do
before do
stub_kubeclient_logs(pod_name, namespace, status: 500)
stub_kubeclient_deployments(namespace, status: 500)
end
it { expect { subject }.to raise_error(::Kubeclient::HttpError) }
......@@ -70,10 +139,10 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
context 'when kubernetes responds with 404s' do
before do
stub_kubeclient_logs(pod_name, namespace, status: 404)
stub_kubeclient_deployments(namespace, status: 404)
end
it { is_expected.to be_empty }
it { is_expected.to include(deployments: []) }
end
end
end
......@@ -2,7 +2,9 @@
require 'spec_helper'
describe Environment do
describe Environment, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
let(:project) { create(:project, :stubbed_repository) }
let(:environment) { create(:environment, project: project) }
......@@ -14,13 +16,16 @@ describe Environment do
end
context 'when environment has a rollout status' do
it 'returns the pod_names' do
pod_name = "pod_1"
let(: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(:deployment, :success, environment: environment)
end
allow_any_instance_of(Gitlab::Kubernetes::RolloutStatus).to receive(:instances)
.and_return([{ pod_name: pod_name }])
it 'returns the pod_names' do
allow(environment).to receive(:rollout_status).and_return(rollout_status)
expect(environment.pod_names).to eq([pod_name])
end
......@@ -98,44 +103,54 @@ describe Environment do
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
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
subject { environment.rollout_status }
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) }
context 'when the environment has rollout status' do
before do
allow(environment).to receive(:has_terminals?).and_return(true)
end
subject { environment.rollout_status }
it 'returns the rollout status from the deployment service' do
expect(environment.deployment_platform)
.to receive(:rollout_status).with(environment)
.and_return(:fake_rollout_status)
context 'cached rollout status is present' do
let(:pods) { %w(pod1 pod2) }
let(:deployments) { %w(deployment1 deployment2) }
is_expected.to eq(:fake_rollout_status)
end
before do
stub_reactive_cache(environment, pods: pods, deployments: deployments)
end
context 'when the environment does not have rollout status' do
before do
allow(environment).to receive(:has_terminals?).and_return(false)
end
it 'fetches the rollout status from the deployment platform' do
expect(environment.deployment_platform).to receive(:rollout_status)
.with(environment, pods: pods, deployments: deployments)
.and_return(:mock_rollout_status)
it { is_expected.to eq(nil) }
is_expected.to eq(:mock_rollout_status)
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 'cached rollout status is not present' do
before do
stub_reactive_cache(environment, nil)
end
context 'when user configured kubernetes from CI/CD > Clusters' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
it 'falls back to a loading status' do
expect(::Gitlab::Kubernetes::RolloutStatus).to receive(:loading).and_return(:mock_loading_status)
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
is_expected.to eq(:mock_loading_status)
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
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