Skip to content
Snippets Groups Projects
Commit 8a4333ab authored by Tiger Watson's avatar Tiger Watson
Browse files

Move deploy board model methods to core

parent 2ad9f918
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -94,9 +94,20 @@ def calculate_reactive_cache_for(environment)
return unless enabled?
 
pods = read_pods(environment.deployment_namespace)
deployments = read_deployments(environment.deployment_namespace)
 
# extract_relevant_pod_data avoids uploading all the pod info into ReactiveCaching
{ pods: extract_relevant_pod_data(pods) }
ingresses = if ::Feature.enabled?(:canary_ingress_weight_control, environment.project, default_enabled: true)
read_ingresses(environment.deployment_namespace)
else
[]
end
# extract only the data required for display to avoid unnecessary caching
{
pods: extract_relevant_pod_data(pods),
deployments: extract_relevant_deployment_data(deployments),
ingresses: extract_relevant_ingress_data(ingresses)
}
end
 
def terminals(environment, data)
Loading
Loading
@@ -109,6 +120,25 @@ def kubeclient
@kubeclient ||= build_kube_client!
end
 
def rollout_status(environment, 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)
ingresses = data[:ingresses].presence || []
::Gitlab::Kubernetes::RolloutStatus.from_deployments(*deployments, pods_attrs: pods, ingresses: ingresses)
end
def ingresses(namespace)
ingresses = read_ingresses(namespace)
ingresses.map { |ingress| ::Gitlab::Kubernetes::Ingress.new(ingress) }
end
def patch_ingress(namespace, ingress, data)
kubeclient.patch_ingress(ingress.name, data, namespace)
end
private
 
def default_namespace(project, environment_name:)
Loading
Loading
@@ -140,6 +170,18 @@ def read_pods(namespace)
[]
end
 
def read_deployments(namespace)
kubeclient.get_deployments(namespace: namespace).as_json
rescue Kubeclient::ResourceNotFoundError
[]
end
def read_ingresses(namespace)
kubeclient.get_ingresses(namespace: namespace).as_json
rescue Kubeclient::ResourceNotFoundError
[]
end
def build_kube_client!
raise "Incomplete settings" unless api_url
 
Loading
Loading
@@ -231,8 +273,24 @@ def extract_relevant_pod_data(pods)
}
end
end
def extract_relevant_deployment_data(deployments)
deployments.map do |deployment|
{
'metadata' => deployment.fetch('metadata', {}).slice('name', 'generation', 'labels', 'annotations'),
'spec' => deployment.fetch('spec', {}).slice('replicas'),
'status' => deployment.fetch('status', {}).slice('observedGeneration')
}
end
end
def extract_relevant_ingress_data(ingresses)
ingresses.map do |ingress|
{
'metadata' => ingress.fetch('metadata', {}).slice('name', 'labels', 'annotations')
}
end
end
end
end
end
Clusters::Platforms::Kubernetes.prepend_if_ee('EE::Clusters::Platforms::Kubernetes')
Loading
Loading
@@ -384,8 +384,38 @@ def elastic_stack_available?
!!deployment_platform&.cluster&.application_elastic_stack_available?
end
 
def rollout_status
return unless rollout_status_available?
result = rollout_status_with_reactive_cache
result || ::Gitlab::Kubernetes::RolloutStatus.loading
end
def ingresses
return unless rollout_status_available?
deployment_platform.ingresses(deployment_namespace)
end
def patch_ingress(ingress, data)
return unless rollout_status_available?
deployment_platform.patch_ingress(deployment_namespace, ingress, data)
end
private
 
def rollout_status_available?
has_terminals?
end
def rollout_status_with_reactive_cache
with_reactive_cache do |data|
deployment_platform.rollout_status(self, data)
end
end
def has_metrics_and_can_query?
has_metrics? && prometheus_adapter.can_query?
end
Loading
Loading
Loading
Loading
@@ -31,6 +31,26 @@ def predefined_variables(project:, environment_name:)
def can_test?
false
end
end
 
MockDeploymentService.prepend_if_ee('EE::MockDeploymentService')
def rollout_status(environment)
case environment.name
when 'staging'
::Gitlab::Kubernetes::RolloutStatus.new([], status: :not_found)
when 'test'
::Gitlab::Kubernetes::RolloutStatus.new([], status: :loading)
else
::Gitlab::Kubernetes::RolloutStatus.new(rollout_status_deployments)
end
end
private
def rollout_status_instances
data = File.read(Rails.root.join('spec', 'fixtures', 'rollout_status_instances.json'))
Gitlab::Json.parse(data)
end
def rollout_status_deployments
[OpenStruct.new(instances: rollout_status_instances)]
end
end
# frozen_string_literal: true
module EE
module Clusters
module Platforms
module Kubernetes
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :calculate_reactive_cache_for
def calculate_reactive_cache_for(environment)
result = super
if result
deployments = read_deployments(environment.deployment_namespace)
ingresses = if ::Feature.enabled?(:canary_ingress_weight_control, environment.project, default_enabled: true)
read_ingresses(environment.deployment_namespace)
else
[]
end
# extract_relevant_deployment_data avoids uploading all the deployment info into ReactiveCaching
result[:deployments] = extract_relevant_deployment_data(deployments)
result[:ingresses] = extract_relevant_ingress_data(ingresses)
end
result
end
def rollout_status(environment, 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)
ingresses = data[:ingresses].presence || []
::Gitlab::Kubernetes::RolloutStatus.from_deployments(*deployments, pods_attrs: pods, ingresses: ingresses)
end
def ingresses(namespace)
ingresses = read_ingresses(namespace)
ingresses.map { |ingress| ::Gitlab::Kubernetes::Ingress.new(ingress) }
end
def patch_ingress(namespace, ingress, data)
kubeclient.patch_ingress(ingress.name, data, namespace)
end
private
def read_deployments(namespace)
kubeclient.get_deployments(namespace: namespace).as_json
rescue Kubeclient::ResourceNotFoundError
[]
end
def read_ingresses(namespace)
kubeclient.get_ingresses(namespace: namespace).as_json
rescue Kubeclient::ResourceNotFoundError
[]
end
def extract_relevant_deployment_data(deployments)
deployments.map do |deployment|
{
'metadata' => deployment.fetch('metadata', {}).slice('name', 'generation', 'labels', 'annotations'),
'spec' => deployment.fetch('spec', {}).slice('replicas'),
'status' => deployment.fetch('status', {}).slice('observedGeneration')
}
end
end
def extract_relevant_ingress_data(ingresses)
ingresses.map do |ingress|
{
'metadata' => ingress.fetch('metadata', {}).slice('name', 'labels', 'annotations')
}
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module DeploymentService
# Environments have a rollout status. This represents the current state of
# deployments to that environment.
def rollout_status(environment)
raise NotImplementedError
end
end
end
Loading
Loading
@@ -66,37 +66,5 @@ def protected?
def protected_deployable_by_user?(user)
project.protected_environment_accessible_to?(name, user)
end
def rollout_status
return unless rollout_status_available?
result = rollout_status_with_reactive_cache
result || ::Gitlab::Kubernetes::RolloutStatus.loading
end
def ingresses
return unless rollout_status_available?
deployment_platform.ingresses(deployment_namespace)
end
def patch_ingress(ingress, data)
return unless rollout_status_available?
deployment_platform.patch_ingress(deployment_namespace, ingress, data)
end
private
def rollout_status_available?
has_terminals?
end
def rollout_status_with_reactive_cache
with_reactive_cache do |data|
deployment_platform.rollout_status(self, data)
end
end
end
end
# frozen_string_literal: true
module EE
module MockDeploymentService
def rollout_status(environment)
case environment.name
when 'staging'
::Gitlab::Kubernetes::RolloutStatus.new([], status: :not_found)
when 'test'
::Gitlab::Kubernetes::RolloutStatus.new([], status: :loading)
else
::Gitlab::Kubernetes::RolloutStatus.new(rollout_status_deployments)
end
end
private
def rollout_status_instances
data = File.read(Rails.root.join('spec', 'fixtures', 'rollout_status_instances.json'))
Gitlab::Json.parse(data)
end
def rollout_status_deployments
[OpenStruct.new(instances: rollout_status_instances)]
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Clusters::Platforms::Kubernetes do
include KubernetesHelpers
include ReactiveCachingHelpers
shared_examples 'resource not found error' do |message|
it 'raises error' do
result = subject
expect(result[:error]).to eq(message)
expect(result[:status]).to eq(:error)
end
end
shared_examples 'kubernetes API error' do |error_code|
it 'raises error' do
result = subject
expect(result[:error]).to eq("Kubernetes API returned status code: #{error_code}")
expect(result[:status]).to eq(:error)
end
end
describe '#rollout_status' do
let(:deployments) { [] }
let(:pods) { [] }
let(:ingresses) { [] }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
let!(:cluster) { create(:cluster, :project, enabled: true, platform_kubernetes: service) }
let(:project) { cluster.project }
let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
let(:cache_data) { Hash(deployments: deployments, pods: pods, ingresses: ingresses) }
subject(:rollout_status) { service.rollout_status(environment, cache_data) }
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
let(:deployments) { [legacy_deployment] }
let(:pods) { [legacy_pod] }
it 'contains nothing' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status.deployments).to eq([])
end
end
context 'deployment with no pods' do
let(:deployment) { kube_deployment(name: 'some-deployment', environment_slug: environment.slug, project_slug: project.full_path_slug) }
let(:deployments) { [deployment] }
let(:pods) { [] }
it 'returns a valid status with matching deployments' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status.deployments.map(&:name)).to contain_exactly('some-deployment')
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) }
let(:deployments) { [matched_deployment, legacy_deployment] }
let(:pods) { [matched_pod, legacy_pod] }
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
end
end
context 'with no deployments but there are pods' do
let(:deployments) do
[]
end
let(:pods) do
[
kube_pod(name: 'pod-1', environment_slug: environment.slug, project_slug: project.full_path_slug),
kube_pod(name: 'pod-2', environment_slug: environment.slug, project_slug: project.full_path_slug)
]
end
it 'returns an empty array' do
expect(rollout_status.instances).to eq([])
end
end
context 'with valid deployments' do
let(:matched_deployment) { kube_deployment(environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 2) }
let(:unmatched_deployment) { kube_deployment }
let(:matched_pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: 'Pending') }
let(:unmatched_pod) { kube_pod(environment_slug: environment.slug + '-test', project_slug: project.full_path_slug) }
let(:deployments) { [matched_deployment, unmatched_deployment] }
let(:pods) { [matched_pod, unmatched_pod] }
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' }
])
expect(rollout_status.instances).to eq([{ pod_name: "kube-pod",
stable: true,
status: "pending",
tooltip: "kube-pod (Pending)",
track: "stable" },
{ pod_name: "Not provided",
stable: true,
status: "pending",
tooltip: "Not provided (Pending)",
track: "stable" }])
end
context 'with canary ingress' do
let(:ingresses) { [kube_ingress(track: :canary)] }
it 'has canary ingress' do
expect(rollout_status).to be_canary_ingress_exists
expect(rollout_status.canary_ingress.canary_weight).to eq(50)
end
end
end
context 'with empty list of deployments' do
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 'when the pod track does not match the deployment track' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1, track: 'weekly')
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug, track: 'weekly'),
kube_pod(name: 'pod-a-2', environment_slug: environment.slug, project_slug: project.full_path_slug, track: 'daily')
]
end
it 'does not return the pod' do
expect(rollout_status.instances.map { |p| p[:pod_name] }).to eq(['pod-a-1'])
end
end
context 'when the pod track is not stable' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1, track: 'something')
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug, track: 'something')
]
end
it 'the pod is not stable' do
expect(rollout_status.instances.map { |p| p.slice(:stable, :track) }).to eq([{ stable: false, track: 'something' }])
end
end
context 'when the pod track is stable' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1, track: 'stable')
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug, track: 'stable')
]
end
it 'the pod is stable' do
expect(rollout_status.instances.map { |p| p.slice(:stable, :track) }).to eq([{ stable: true, track: 'stable' }])
end
end
context 'when the pod track is not provided' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1)
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug)
]
end
it 'the pod is stable' do
expect(rollout_status.instances.map { |p| p.slice(:stable, :track) }).to eq([{ stable: true, track: 'stable' }])
end
end
context 'when the number of matching pods does not match the number of replicas' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 3)
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug)
]
end
it 'returns a pending pod for each missing replica' do
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status) }).to eq([
{ pod_name: 'pod-a-1', status: 'running' },
{ pod_name: 'Not provided', status: 'pending' },
{ pod_name: 'Not provided', status: 'pending' }
])
end
end
context 'when pending pods are returned for missing replicas' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 2, track: 'canary'),
kube_deployment(name: 'deployment-b', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 2, track: 'stable')
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug, track: 'canary')
]
end
it 'returns the correct track for the pending pods' do
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status, :track) }).to eq([
{ pod_name: 'pod-a-1', status: 'running', track: 'canary' },
{ pod_name: 'Not provided', status: 'pending', track: 'canary' },
{ pod_name: 'Not provided', status: 'pending', track: 'stable' },
{ pod_name: 'Not provided', status: 'pending', track: 'stable' }
])
end
end
context 'when two deployments with the same track are missing instances' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1, track: 'mytrack'),
kube_deployment(name: 'deployment-b', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1, track: 'mytrack')
]
end
let(:pods) do
[]
end
it 'returns the correct number of pending pods' do
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status, :track) }).to eq([
{ pod_name: 'Not provided', status: 'pending', track: 'mytrack' },
{ pod_name: 'Not provided', status: 'pending', track: 'mytrack' }
])
end
end
context 'with multiple matching deployments' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 2),
kube_deployment(name: 'deployment-b', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 2)
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug),
kube_pod(name: 'pod-a-2', environment_slug: environment.slug, project_slug: project.full_path_slug),
kube_pod(name: 'pod-b-1', environment_slug: environment.slug, project_slug: project.full_path_slug),
kube_pod(name: 'pod-b-2', environment_slug: environment.slug, project_slug: project.full_path_slug)
]
end
it 'returns each pod once' do
expect(rollout_status.instances.map { |p| p[:pod_name] }).to eq(['pod-a-1', 'pod-a-2', 'pod-b-1', 'pod-b-2'])
end
end
end
describe '#calculate_reactive_cache_for' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:namespace) { 'project-namespace' }
let(:environment) { instance_double(Environment, deployment_namespace: namespace, project: cluster.project) }
let(:expected_pod_cached_data) do
kube_pod.tap { |kp| kp['metadata'].delete('namespace') }
end
subject { service.calculate_reactive_cache_for(environment) }
context 'when kubernetes responds with valid deployments' do
before do
stub_kubeclient_pods(namespace)
stub_kubeclient_deployments(namespace)
stub_kubeclient_ingresses(namespace)
end
shared_examples 'successful deployment request' do
it { is_expected.to include(pods: [expected_pod_cached_data], deployments: [kube_deployment], ingresses: [kube_ingress]) }
end
context 'on a project level cluster' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
include_examples 'successful deployment request'
end
context 'on a group level cluster' do
let(:cluster) { create(:cluster, :group, platform_kubernetes: service) }
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
context 'when canary_ingress_weight_control feature flag is disabled' do
before do
stub_feature_flags(canary_ingress_weight_control: false)
end
it 'does not fetch ingress data from kubernetes' do
expect(subject[:ingresses]).to be_empty
end
end
end
context 'when kubernetes responds with 500s' do
before do
stub_kubeclient_pods(namespace)
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)
stub_kubeclient_deployments(namespace, status: 404)
stub_kubeclient_ingresses(namespace, status: 404)
end
it { is_expected.to include(deployments: [], ingresses: []) }
end
end
describe '#ingresses' do
subject { service.ingresses(namespace) }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:namespace) { 'project-namespace' }
context 'when there is an ingress in the namespace' do
before do
stub_kubeclient_ingresses(namespace)
end
it 'returns an ingress' do
expect(subject.count).to eq(1)
expect(subject.first).to be_kind_of(::Gitlab::Kubernetes::Ingress)
expect(subject.first.name).to eq('production-auto-deploy')
end
end
context 'when there are no ingresss in the namespace' do
before do
allow(service.kubeclient).to receive(:get_ingresses) { raise Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil) }
end
it 'returns nothing' do
is_expected.to be_empty
end
end
end
describe '#patch_ingress' do
subject { service.patch_ingress(namespace, ingress, data) }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:namespace) { 'project-namespace' }
let(:ingress) { Gitlab::Kubernetes::Ingress.new(kube_ingress) }
let(:data) { { metadata: { annotations: { name: 'test' } } } }
context 'when there is an ingress in the namespace' do
before do
stub_kubeclient_ingresses(namespace, method: :patch, resource_path: "/#{ingress.name}")
end
it 'returns an ingress' do
expect(subject[:items][0][:metadata][:name]).to eq('production-auto-deploy')
end
end
context 'when there are no ingresss in the namespace' do
before do
allow(service.kubeclient).to receive(:patch_ingress) { raise Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil) }
end
it 'raises an error' do
expect { subject }.to raise_error(Kubeclient::ResourceNotFoundError)
end
end
end
end
Loading
Loading
@@ -184,123 +184,4 @@
end
end
end
describe '#rollout_status' do
let!(:cluster) { create(:cluster, :project, :provided_by_user, projects: [project]) }
let!(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, :success, environment: environment, project: project) }
subject { environment.rollout_status }
context 'environment does not have a deployment board available' do
before do
allow(environment).to receive(:has_terminals?).and_return(false)
end
it { is_expected.to be_nil }
end
context 'cached rollout status is present' do
let(:pods) { %w(pod1 pod2) }
let(:deployments) { %w(deployment1 deployment2) }
before do
stub_reactive_cache(environment, pods: pods, deployments: deployments)
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)
is_expected.to eq(:mock_rollout_status)
end
end
context 'cached rollout status is not present yet' do
before do
stub_reactive_cache(environment, nil)
end
it 'falls back to a loading status' do
expect(::Gitlab::Kubernetes::RolloutStatus).to receive(:loading).and_return(:mock_loading_status)
is_expected.to eq(:mock_loading_status)
end
end
end
describe '#ingresses' do
subject { environment.ingresses }
let(:deployment_platform) { double(:deployment_platform) }
let(:deployment_namespace) { 'production' }
before do
allow(environment).to receive(:deployment_platform) { deployment_platform }
allow(environment).to receive(:deployment_namespace) { deployment_namespace }
end
context 'when rollout status is available' do
before do
allow(environment).to receive(:rollout_status_available?) { true }
end
it 'fetches ingresses from the deployment platform' do
expect(deployment_platform).to receive(:ingresses).with(deployment_namespace)
subject
end
end
context 'when rollout status is not available' do
before do
allow(environment).to receive(:rollout_status_available?) { false }
end
it 'does nothing' do
expect(deployment_platform).not_to receive(:ingresses)
subject
end
end
end
describe '#patch_ingress' do
subject { environment.patch_ingress(ingress, data) }
let(:ingress) { double(:ingress) }
let(:data) { double(:data) }
let(:deployment_platform) { double(:deployment_platform) }
let(:deployment_namespace) { 'production' }
before do
allow(environment).to receive(:deployment_platform) { deployment_platform }
allow(environment).to receive(:deployment_namespace) { deployment_namespace }
end
context 'when rollout status is available' do
before do
allow(environment).to receive(:rollout_status_available?) { true }
end
it 'fetches ingresses from the deployment platform' do
expect(deployment_platform).to receive(:patch_ingress).with(deployment_namespace, ingress, data)
subject
end
end
context 'when rollout status is not available' do
before do
allow(environment).to receive(:rollout_status_available?) { false }
end
it 'does nothing' do
expect(deployment_platform).not_to receive(:patch_ingress)
subject
end
end
end
end
Loading
Loading
@@ -4,6 +4,7 @@
 
RSpec.describe Clusters::Platforms::Kubernetes do
include KubernetesHelpers
include ReactiveCachingHelpers
 
it { is_expected.to belong_to(:cluster) }
it { is_expected.to be_kind_of(Gitlab::Kubernetes) }
Loading
Loading
@@ -406,32 +407,62 @@
end
 
describe '#calculate_reactive_cache_for' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:namespace) { 'project-namespace' }
let(:environment) { instance_double(Environment, deployment_namespace: namespace, project: cluster.project) }
let(:expected_pod_cached_data) do
kube_pod.tap { |kp| kp['metadata'].delete('namespace') }
end
 
let(:namespace) { "project-namespace" }
let(:environment) { instance_double(Environment, deployment_namespace: namespace, project: service.cluster.project) }
subject { service.calculate_reactive_cache_for(environment) }
 
context 'when the kubernetes integration is disabled' do
context 'when kubernetes responds with valid deployments' do
before do
allow(service).to receive(:enabled?).and_return(false)
stub_kubeclient_pods(namespace)
stub_kubeclient_deployments(namespace)
stub_kubeclient_ingresses(namespace)
end
 
it { is_expected.to be_nil }
shared_examples 'successful deployment request' do
it { is_expected.to include(pods: [expected_pod_cached_data], deployments: [kube_deployment], ingresses: [kube_ingress]) }
end
context 'on a project level cluster' do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
include_examples 'successful deployment request'
end
context 'on a group level cluster' do
let(:cluster) { create(:cluster, :group, platform_kubernetes: service) }
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
context 'when canary_ingress_weight_control feature flag is disabled' do
before do
stub_feature_flags(canary_ingress_weight_control: false)
end
it 'does not fetch ingress data from kubernetes' do
expect(subject[:ingresses]).to be_empty
end
end
end
 
context 'when kubernetes responds with valid pods and deployments' do
context 'when the kubernetes integration is disabled' do
before do
stub_kubeclient_pods(namespace)
stub_kubeclient_deployments(namespace)
stub_kubeclient_ingresses(namespace)
allow(service).to receive(:enabled?).and_return(false)
end
 
it { is_expected.to include(pods: [expected_pod_cached_data]) }
it { is_expected.to be_nil }
end
 
context 'when kubernetes responds with 500s' do
Loading
Loading
@@ -451,7 +482,351 @@
stub_kubeclient_ingresses(namespace, status: 404)
end
 
it { is_expected.to include(pods: []) }
it { is_expected.to eq(pods: [], deployments: [], ingresses: []) }
end
end
describe '#rollout_status' do
let(:deployments) { [] }
let(:pods) { [] }
let(:ingresses) { [] }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
let!(:cluster) { create(:cluster, :project, enabled: true, platform_kubernetes: service) }
let(:project) { cluster.project }
let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
let(:cache_data) { Hash(deployments: deployments, pods: pods, ingresses: ingresses) }
subject(:rollout_status) { service.rollout_status(environment, cache_data) }
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
let(:deployments) { [legacy_deployment] }
let(:pods) { [legacy_pod] }
it 'contains nothing' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status.deployments).to eq([])
end
end
context 'deployment with no pods' do
let(:deployment) { kube_deployment(name: 'some-deployment', environment_slug: environment.slug, project_slug: project.full_path_slug) }
let(:deployments) { [deployment] }
let(:pods) { [] }
it 'returns a valid status with matching deployments' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status.deployments.map(&:name)).to contain_exactly('some-deployment')
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) }
let(:deployments) { [matched_deployment, legacy_deployment] }
let(:pods) { [matched_pod, legacy_pod] }
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
end
end
context 'with no deployments but there are pods' do
let(:deployments) do
[]
end
let(:pods) do
[
kube_pod(name: 'pod-1', environment_slug: environment.slug, project_slug: project.full_path_slug),
kube_pod(name: 'pod-2', environment_slug: environment.slug, project_slug: project.full_path_slug)
]
end
it 'returns an empty array' do
expect(rollout_status.instances).to eq([])
end
end
context 'with valid deployments' do
let(:matched_deployment) { kube_deployment(environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 2) }
let(:unmatched_deployment) { kube_deployment }
let(:matched_pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: 'Pending') }
let(:unmatched_pod) { kube_pod(environment_slug: environment.slug + '-test', project_slug: project.full_path_slug) }
let(:deployments) { [matched_deployment, unmatched_deployment] }
let(:pods) { [matched_pod, unmatched_pod] }
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' }
])
expect(rollout_status.instances).to eq([{ pod_name: "kube-pod",
stable: true,
status: "pending",
tooltip: "kube-pod (Pending)",
track: "stable" },
{ pod_name: "Not provided",
stable: true,
status: "pending",
tooltip: "Not provided (Pending)",
track: "stable" }])
end
context 'with canary ingress' do
let(:ingresses) { [kube_ingress(track: :canary)] }
it 'has canary ingress' do
expect(rollout_status).to be_canary_ingress_exists
expect(rollout_status.canary_ingress.canary_weight).to eq(50)
end
end
end
context 'with empty list of deployments' do
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 'when the pod track does not match the deployment track' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1, track: 'weekly')
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug, track: 'weekly'),
kube_pod(name: 'pod-a-2', environment_slug: environment.slug, project_slug: project.full_path_slug, track: 'daily')
]
end
it 'does not return the pod' do
expect(rollout_status.instances.map { |p| p[:pod_name] }).to eq(['pod-a-1'])
end
end
context 'when the pod track is not stable' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1, track: 'something')
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug, track: 'something')
]
end
it 'the pod is not stable' do
expect(rollout_status.instances.map { |p| p.slice(:stable, :track) }).to eq([{ stable: false, track: 'something' }])
end
end
context 'when the pod track is stable' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1, track: 'stable')
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug, track: 'stable')
]
end
it 'the pod is stable' do
expect(rollout_status.instances.map { |p| p.slice(:stable, :track) }).to eq([{ stable: true, track: 'stable' }])
end
end
context 'when the pod track is not provided' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1)
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug)
]
end
it 'the pod is stable' do
expect(rollout_status.instances.map { |p| p.slice(:stable, :track) }).to eq([{ stable: true, track: 'stable' }])
end
end
context 'when the number of matching pods does not match the number of replicas' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 3)
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug)
]
end
it 'returns a pending pod for each missing replica' do
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status) }).to eq([
{ pod_name: 'pod-a-1', status: 'running' },
{ pod_name: 'Not provided', status: 'pending' },
{ pod_name: 'Not provided', status: 'pending' }
])
end
end
context 'when pending pods are returned for missing replicas' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 2, track: 'canary'),
kube_deployment(name: 'deployment-b', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 2, track: 'stable')
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug, track: 'canary')
]
end
it 'returns the correct track for the pending pods' do
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status, :track) }).to eq([
{ pod_name: 'pod-a-1', status: 'running', track: 'canary' },
{ pod_name: 'Not provided', status: 'pending', track: 'canary' },
{ pod_name: 'Not provided', status: 'pending', track: 'stable' },
{ pod_name: 'Not provided', status: 'pending', track: 'stable' }
])
end
end
context 'when two deployments with the same track are missing instances' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1, track: 'mytrack'),
kube_deployment(name: 'deployment-b', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 1, track: 'mytrack')
]
end
let(:pods) do
[]
end
it 'returns the correct number of pending pods' do
expect(rollout_status.instances.map { |p| p.slice(:pod_name, :status, :track) }).to eq([
{ pod_name: 'Not provided', status: 'pending', track: 'mytrack' },
{ pod_name: 'Not provided', status: 'pending', track: 'mytrack' }
])
end
end
context 'with multiple matching deployments' do
let(:deployments) do
[
kube_deployment(name: 'deployment-a', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 2),
kube_deployment(name: 'deployment-b', environment_slug: environment.slug, project_slug: project.full_path_slug, replicas: 2)
]
end
let(:pods) do
[
kube_pod(name: 'pod-a-1', environment_slug: environment.slug, project_slug: project.full_path_slug),
kube_pod(name: 'pod-a-2', environment_slug: environment.slug, project_slug: project.full_path_slug),
kube_pod(name: 'pod-b-1', environment_slug: environment.slug, project_slug: project.full_path_slug),
kube_pod(name: 'pod-b-2', environment_slug: environment.slug, project_slug: project.full_path_slug)
]
end
it 'returns each pod once' do
expect(rollout_status.instances.map { |p| p[:pod_name] }).to eq(['pod-a-1', 'pod-a-2', 'pod-b-1', 'pod-b-2'])
end
end
end
describe '#ingresses' do
subject { service.ingresses(namespace) }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:namespace) { 'project-namespace' }
context 'when there is an ingress in the namespace' do
before do
stub_kubeclient_ingresses(namespace)
end
it 'returns an ingress' do
expect(subject.count).to eq(1)
expect(subject.first).to be_kind_of(::Gitlab::Kubernetes::Ingress)
expect(subject.first.name).to eq('production-auto-deploy')
end
end
context 'when there are no ingresss in the namespace' do
before do
allow(service.kubeclient).to receive(:get_ingresses) { raise Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil) }
end
it 'returns nothing' do
is_expected.to be_empty
end
end
end
describe '#patch_ingress' do
subject { service.patch_ingress(namespace, ingress, data) }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:namespace) { 'project-namespace' }
let(:ingress) { Gitlab::Kubernetes::Ingress.new(kube_ingress) }
let(:data) { { metadata: { annotations: { name: 'test' } } } }
context 'when there is an ingress in the namespace' do
before do
stub_kubeclient_ingresses(namespace, method: :patch, resource_path: "/#{ingress.name}")
end
it 'returns an ingress' do
expect(subject[:items][0][:metadata][:name]).to eq('production-auto-deploy')
end
end
context 'when there are no ingresss in the namespace' do
before do
allow(service.kubeclient).to receive(:patch_ingress) { raise Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil) }
end
it 'raises an error' do
expect { subject }.to raise_error(Kubeclient::ResourceNotFoundError)
end
end
end
end
Loading
Loading
@@ -1421,4 +1421,123 @@
end
end
end
describe '#rollout_status' do
let!(:cluster) { create(:cluster, :project, :provided_by_user, projects: [project]) }
let!(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, :success, environment: environment, project: project) }
subject { environment.rollout_status }
context 'environment does not have a deployment board available' do
before do
allow(environment).to receive(:has_terminals?).and_return(false)
end
it { is_expected.to be_nil }
end
context 'cached rollout status is present' do
let(:pods) { %w(pod1 pod2) }
let(:deployments) { %w(deployment1 deployment2) }
before do
stub_reactive_cache(environment, pods: pods, deployments: deployments)
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)
is_expected.to eq(:mock_rollout_status)
end
end
context 'cached rollout status is not present yet' do
before do
stub_reactive_cache(environment, nil)
end
it 'falls back to a loading status' do
expect(::Gitlab::Kubernetes::RolloutStatus).to receive(:loading).and_return(:mock_loading_status)
is_expected.to eq(:mock_loading_status)
end
end
end
describe '#ingresses' do
subject { environment.ingresses }
let(:deployment_platform) { double(:deployment_platform) }
let(:deployment_namespace) { 'production' }
before do
allow(environment).to receive(:deployment_platform) { deployment_platform }
allow(environment).to receive(:deployment_namespace) { deployment_namespace }
end
context 'when rollout status is available' do
before do
allow(environment).to receive(:rollout_status_available?) { true }
end
it 'fetches ingresses from the deployment platform' do
expect(deployment_platform).to receive(:ingresses).with(deployment_namespace)
subject
end
end
context 'when rollout status is not available' do
before do
allow(environment).to receive(:rollout_status_available?) { false }
end
it 'does nothing' do
expect(deployment_platform).not_to receive(:ingresses)
subject
end
end
end
describe '#patch_ingress' do
subject { environment.patch_ingress(ingress, data) }
let(:ingress) { double(:ingress) }
let(:data) { double(:data) }
let(:deployment_platform) { double(:deployment_platform) }
let(:deployment_namespace) { 'production' }
before do
allow(environment).to receive(:deployment_platform) { deployment_platform }
allow(environment).to receive(:deployment_namespace) { deployment_namespace }
end
context 'when rollout status is available' do
before do
allow(environment).to receive(:rollout_status_available?) { true }
end
it 'fetches ingresses from the deployment platform' do
expect(deployment_platform).to receive(:patch_ingress).with(deployment_namespace, ingress, data)
subject
end
end
context 'when rollout status is not available' do
before do
allow(environment).to receive(:rollout_status_available?) { false }
end
it 'does nothing' do
expect(deployment_platform).not_to receive(:patch_ingress)
subject
end
end
end
end
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