summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-30 18:09:46 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-30 18:09:46 +0000
commitace0df53d3ed38344b470727d430484d24eeb798 (patch)
treeec1fc71d793bf3d588df9fe97c4649c87e697e73 /spec
parent56eafa995d0bbda39bc24cd07537286bf36a4dd9 (diff)
downloadgitlab-ce-ace0df53d3ed38344b470727d430484d24eeb798.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/db/schema_spec.rb2
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb41
-rw-r--r--spec/fixtures/api/schemas/entities/note_user_entity.json3
-rw-r--r--spec/frontend/diffs/components/app_spec.js10
-rw-r--r--spec/frontend/notes/components/note_header_spec.js37
-rw-r--r--spec/helpers/markup_helper_spec.rb18
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/kubernetes/deployment_spec.rb190
-rw-r--r--spec/lib/gitlab/kubernetes/ingress_spec.rb57
-rw-r--r--spec/lib/gitlab/kubernetes/rollout_instances_spec.rb128
-rw-r--r--spec/lib/gitlab/kubernetes/rollout_status_spec.rb271
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb399
-rw-r--r--spec/models/environment_spec.rb119
-rw-r--r--spec/serializers/rollout_status_entity_spec.rb53
-rw-r--r--spec/serializers/rollout_statuses/ingress_entity_spec.rb19
15 files changed, 1329 insertions, 22 deletions
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index c35f3831a58..a06ba4f229a 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe 'Database schema' do
users_star_projects: %w[user_id],
vulnerability_identifiers: %w[external_id],
vulnerability_scanners: %w[external_id],
- web_hooks: %w[service_id group_id]
+ web_hooks: %w[group_id]
}.with_indifferent_access.freeze
context 'for table' do
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index d0340dfc880..f341709b73d 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User edit profile' do
+ include Spec::Support::Helpers::Features::NotesHelpers
+
let(:user) { create(:user) }
before do
@@ -398,6 +400,45 @@ RSpec.describe 'User edit profile' do
end
end
+ context 'note header' do
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:emoji) { "stuffed_flatbread" }
+
+ before do
+ project.add_guest(user)
+ create(:user_status, user: user, message: 'Taking notes', emoji: emoji)
+
+ visit(project_issue_path(project, issue))
+
+ add_note("This is a comment")
+ visit(project_issue_path(project, issue))
+
+ wait_for_requests
+ end
+
+ it 'displays the status emoji' do
+ first_note = page.find_all(".main-notes-list .timeline-entry").first
+
+ expect(first_note).to have_emoji(emoji)
+ end
+
+ it 'clears the status emoji' do
+ open_edit_status_modal
+
+ page.within "#set-user-status-modal" do
+ click_button 'Remove status'
+ end
+
+ visit(project_issue_path(project, issue))
+ wait_for_requests
+
+ first_note = page.find_all(".main-notes-list .timeline-entry").first
+
+ expect(first_note).not_to have_css('.user-status-emoji')
+ end
+ end
+
context 'with set_user_availability_status feature flag disabled' do
before do
stub_feature_flags(set_user_availability_status: false)
diff --git a/spec/fixtures/api/schemas/entities/note_user_entity.json b/spec/fixtures/api/schemas/entities/note_user_entity.json
index 4a27d885cdc..e2bbaad7201 100644
--- a/spec/fixtures/api/schemas/entities/note_user_entity.json
+++ b/spec/fixtures/api/schemas/entities/note_user_entity.json
@@ -15,6 +15,7 @@
"path": { "type": "string" },
"name": { "type": "string" },
"username": { "type": "string" },
- "status_tooltip_html": { "$ref": "../types/nullable_string.json" }
+ "status_tooltip_html": { "$ref": "../types/nullable_string.json" },
+ "show_status": { "type": "boolean" }
}
}
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 5982b88737c..01c789270da 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -678,8 +678,12 @@ describe('diffs/components/app', () => {
expect(setShowTreeList).toHaveBeenCalledWith({ showTreeList: true, saving: false });
});
- it('calls setShowTreeList with localstorage value', () => {
- localStorage.setItem('mr_tree_show', 'true');
+ it.each`
+ showTreeList
+ ${true}
+ ${false}
+ `('calls setShowTreeList with localstorage $showTreeList', ({ showTreeList }) => {
+ localStorage.setItem('mr_tree_show', showTreeList);
createComponent({}, ({ state }) => {
state.diffs.diffFiles.push({ sha: '123' });
@@ -691,7 +695,7 @@ describe('diffs/components/app', () => {
wrapper.vm.setTreeDisplay();
- expect(setShowTreeList).toHaveBeenCalledWith({ showTreeList: true, saving: false });
+ expect(setShowTreeList).toHaveBeenCalledWith({ showTreeList, saving: false });
});
});
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 69aab0d051e..1c6d0bafda8 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -22,6 +22,10 @@ describe('NoteHeader component', () => {
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
const findConfidentialIndicator = () => wrapper.find('[data-testid="confidentialIndicator"]');
const findSpinner = () => wrapper.find({ ref: 'spinner' });
+ const findAuthorStatus = () => wrapper.find({ ref: 'authorStatus' });
+
+ const statusHtml =
+ '"<span class="user-status-emoji has-tooltip" title="foo bar" data-html="true" data-placement="top"><gl-emoji title="basketball and hoop" data-name="basketball" data-unicode-version="6.0">🏀</gl-emoji></span>"';
const author = {
avatar_url: null,
@@ -30,6 +34,8 @@ describe('NoteHeader component', () => {
path: '/root',
state: 'active',
username: 'root',
+ show_status: true,
+ status_tooltip_html: statusHtml,
status: {
availability: '',
},
@@ -109,6 +115,32 @@ describe('NoteHeader component', () => {
expect(wrapper.find('.js-user-link').text()).toContain('(Busy)');
});
+ it('renders author status', () => {
+ createComponent({ author });
+
+ expect(findAuthorStatus().exists()).toBe(true);
+ });
+
+ it('does not render author status if show_status=false', () => {
+ createComponent({
+ author: { ...author, status: { availability: AVAILABILITY_STATUS.BUSY }, show_status: false },
+ });
+
+ expect(findAuthorStatus().exists()).toBe(false);
+ });
+
+ it('does not render author status if status_tooltip_html=null', () => {
+ createComponent({
+ author: {
+ ...author,
+ status: { availability: AVAILABILITY_STATUS.BUSY },
+ status_tooltip_html: null,
+ },
+ });
+
+ expect(findAuthorStatus().exists()).toBe(false);
+ });
+
it('renders deleted user text if author is not passed as a prop', () => {
createComponent();
@@ -206,13 +238,12 @@ describe('NoteHeader component', () => {
createComponent({
author: {
...author,
- status_tooltip_html:
- '"<span class="user-status-emoji has-tooltip" title="foo bar" data-html="true" data-placement="top"><gl-emoji title="basketball and hoop" data-name="basketball" data-unicode-version="6.0">🏀</gl-emoji></span>"',
+ status_tooltip_html: statusHtml,
},
});
return nextTick().then(() => {
- const authorStatus = wrapper.find({ ref: 'authorStatus' });
+ const authorStatus = findAuthorStatus();
authorStatus.trigger('mouseenter');
expect(authorStatus.find('gl-emoji').attributes('title')).toBeUndefined();
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 6c5855eeb91..45e8a2e7e1a 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -316,6 +316,7 @@ RSpec.describe MarkupHelper do
describe '#render_wiki_content' do
let(:wiki) { double('WikiPage', path: "file.#{extension}") }
let(:wiki_repository) { double('Repository') }
+ let(:content) { 'wiki content' }
let(:context) do
{
pipeline: :wiki, project: project, wiki: wiki,
@@ -325,9 +326,11 @@ RSpec.describe MarkupHelper do
end
before do
- expect(wiki).to receive(:content).and_return('wiki content')
+ expect(wiki).to receive(:content).and_return(content)
expect(wiki).to receive(:slug).and_return('nested/page')
expect(wiki).to receive(:repository).and_return(wiki_repository)
+ allow(wiki).to receive(:container).and_return(project)
+
helper.instance_variable_set(:@wiki, wiki)
end
@@ -339,6 +342,19 @@ RSpec.describe MarkupHelper do
helper.render_wiki_content(wiki)
end
+
+ context 'when context has labels' do
+ let_it_be(:label) { create(:label, title: 'Bug', project: project) }
+
+ let(:content) { '~Bug' }
+
+ it 'renders label' do
+ result = helper.render_wiki_content(wiki)
+ doc = Nokogiri::HTML.parse(result)
+
+ expect(doc.css('.gl-label-link')).not_to be_empty
+ end
+ end
end
context 'when file is Asciidoc' do
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb
index bed428e0327..2999dc5bb41 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb
@@ -28,7 +28,9 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestMergedByImporter, :cle
end
it 'adds a note referencing the merger user when the user cannot be mapped' do
- expect { subject.execute }.to change(Note, :count).by(1)
+ expect { subject.execute }
+ .to change(Note, :count).by(1)
+ .and not_change(merge_request, :updated_at)
last_note = merge_request.notes.last
diff --git a/spec/lib/gitlab/kubernetes/deployment_spec.rb b/spec/lib/gitlab/kubernetes/deployment_spec.rb
new file mode 100644
index 00000000000..2433e854e5b
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/deployment_spec.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::Deployment do
+ include KubernetesHelpers
+
+ let(:pods) { {} }
+
+ subject(:deployment) { described_class.new(params, pods: pods) }
+
+ describe '#name' do
+ let(:params) { named(:selected) }
+
+ it { expect(deployment.name).to eq(:selected) }
+ end
+
+ describe '#labels' do
+ let(:params) { make('metadata', 'labels' => :selected) }
+
+ it { expect(deployment.labels).to eq(:selected) }
+ end
+
+ describe '#outdated?' do
+ context 'when outdated' do
+ let(:params) { generation(2, 1, 0) }
+
+ it { expect(deployment.outdated?).to be_truthy }
+ end
+
+ context 'when up to date' do
+ let(:params) { generation(2, 2, 0) }
+
+ it { expect(deployment.outdated?).to be_falsy }
+ end
+
+ context 'when ahead of latest' do
+ let(:params) { generation(1, 2, 0) }
+
+ it { expect(deployment.outdated?).to be_falsy }
+ end
+ end
+
+ describe '#instances' do
+ context 'when unnamed' do
+ let(:pods) do
+ [
+ kube_pod(name: nil, status: 'Pending'),
+ kube_pod(name: nil, status: 'Pending'),
+ kube_pod(name: nil, status: 'Pending'),
+ kube_pod(name: nil, status: 'Pending')
+ ]
+ end
+
+ let(:params) { combine(generation(1, 1, 4)) }
+
+ it 'returns all pods with generated names and pending' do
+ expected = [
+ { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+
+ # When replica count is higher than pods it is considered that pod was not
+ # able to spawn for some reason like limited resources.
+ context 'when number of pods is less than wanted replicas' do
+ let(:wanted_replicas) { 3 }
+ let(:pods) { [kube_pod(name: nil, status: 'Running')] }
+ let(:params) { combine(generation(1, 1, wanted_replicas)) }
+
+ it 'returns not spawned pods as pending and unknown and running' do
+ expected = [
+ { status: 'running', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Running)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'Not provided', tooltip: 'Not provided (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'Not provided', tooltip: 'Not provided (Pending)', track: 'stable', stable: true }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+
+ context 'when outdated' do
+ let(:pods) do
+ [
+ kube_pod(status: 'Pending'),
+ kube_pod(name: 'kube-pod1', status: 'Pending'),
+ kube_pod(name: 'kube-pod2', status: 'Pending'),
+ kube_pod(name: 'kube-pod3', status: 'Pending')
+ ]
+ end
+
+ let(:params) { combine(named('foo'), generation(1, 0, 4)) }
+
+ it 'returns all instances as named and waiting' do
+ expected = [
+ { status: 'pending', pod_name: 'kube-pod', tooltip: 'kube-pod (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'kube-pod1', tooltip: 'kube-pod1 (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'kube-pod2', tooltip: 'kube-pod2 (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'kube-pod3', tooltip: 'kube-pod3 (Pending)', track: 'stable', stable: true }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+
+ context 'with pods of each type' do
+ let(:pods) do
+ [
+ kube_pod(status: 'Succeeded'),
+ kube_pod(name: 'kube-pod1', status: 'Running'),
+ kube_pod(name: 'kube-pod2', status: 'Pending'),
+ kube_pod(name: 'kube-pod3', status: 'Pending')
+ ]
+ end
+
+ let(:params) { combine(named('foo'), generation(1, 1, 4)) }
+
+ it 'returns all instances' do
+ expected = [
+ { status: 'succeeded', pod_name: 'kube-pod', tooltip: 'kube-pod (Succeeded)', track: 'stable', stable: true },
+ { status: 'running', pod_name: 'kube-pod1', tooltip: 'kube-pod1 (Running)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'kube-pod2', tooltip: 'kube-pod2 (Pending)', track: 'stable', stable: true },
+ { status: 'pending', pod_name: 'kube-pod3', tooltip: 'kube-pod3 (Pending)', track: 'stable', stable: true }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+
+ context 'with track label' do
+ let(:pods) { [kube_pod(status: 'Pending')] }
+ let(:labels) { { 'track' => track } }
+ let(:params) { combine(named('foo', labels), generation(1, 0, 1)) }
+
+ context 'when marked as stable' do
+ let(:track) { 'stable' }
+
+ it 'returns all instances' do
+ expected = [
+ { status: 'pending', pod_name: 'kube-pod', tooltip: 'kube-pod (Pending)', track: 'stable', stable: true }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+
+ context 'when marked as canary' do
+ let(:track) { 'canary' }
+ let(:pods) { [kube_pod(status: 'Pending', track: track)] }
+
+ it 'returns all instances' do
+ expected = [
+ { status: 'pending', pod_name: 'kube-pod', tooltip: 'kube-pod (Pending)', track: 'canary', stable: false }
+ ]
+
+ expect(deployment.instances).to eq(expected)
+ end
+ end
+ end
+ end
+
+ def generation(expected, observed, replicas)
+ combine(
+ make('metadata', 'generation' => expected),
+ make('status', 'observedGeneration' => observed),
+ make('spec', 'replicas' => replicas)
+ )
+ end
+
+ def named(name = "foo", labels = {})
+ make('metadata', 'name' => name, 'labels' => labels)
+ end
+
+ def make(key, values = {})
+ hsh = {}
+ hsh[key] = values
+ hsh
+ end
+
+ def combine(*hashes)
+ out = {}
+ hashes.each { |hsh| out = out.deep_merge(hsh) }
+ out
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/ingress_spec.rb b/spec/lib/gitlab/kubernetes/ingress_spec.rb
new file mode 100644
index 00000000000..e4d6bf4086f
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/ingress_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::Ingress do
+ include KubernetesHelpers
+
+ let(:ingress) { described_class.new(params) }
+
+ describe '#canary?' do
+ subject { ingress.canary? }
+
+ context 'with canary ingress parameters' do
+ let(:params) { canary_metadata }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with stable ingress parameters' do
+ let(:params) { stable_metadata }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#canary_weight' do
+ subject { ingress.canary_weight }
+
+ context 'with canary ingress parameters' do
+ let(:params) { canary_metadata }
+
+ it { is_expected.to eq(50) }
+ end
+
+ context 'with stable ingress parameters' do
+ let(:params) { stable_metadata }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#name' do
+ subject { ingress.name }
+
+ let(:params) { stable_metadata }
+
+ it { is_expected.to eq('production-auto-deploy') }
+ end
+
+ def stable_metadata
+ kube_ingress(track: :stable)
+ end
+
+ def canary_metadata
+ kube_ingress(track: :canary)
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/rollout_instances_spec.rb b/spec/lib/gitlab/kubernetes/rollout_instances_spec.rb
new file mode 100644
index 00000000000..3ac97ddc75d
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/rollout_instances_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::RolloutInstances do
+ include KubernetesHelpers
+
+ def setup(deployments_attrs, pods_attrs)
+ deployments = deployments_attrs.map do |attrs|
+ ::Gitlab::Kubernetes::Deployment.new(attrs, pods: pods_attrs)
+ end
+
+ pods = pods_attrs.map do |attrs|
+ ::Gitlab::Kubernetes::Pod.new(attrs)
+ end
+
+ [deployments, pods]
+ end
+
+ describe '#pod_instances' do
+ it 'returns an instance for a deployment with one pod' do
+ deployments, pods = setup(
+ [kube_deployment(name: 'one', track: 'stable', replicas: 1)],
+ [kube_pod(name: 'one', status: 'Running', track: 'stable')]
+ )
+ rollout_instances = described_class.new(deployments, pods)
+
+ expect(rollout_instances.pod_instances).to eq([{
+ pod_name: 'one',
+ stable: true,
+ status: 'running',
+ tooltip: 'one (Running)',
+ track: 'stable'
+ }])
+ end
+
+ it 'returns a pending pod for a missing replica' do
+ deployments, pods = setup(
+ [kube_deployment(name: 'one', track: 'stable', replicas: 1)],
+ []
+ )
+ rollout_instances = described_class.new(deployments, pods)
+
+ expect(rollout_instances.pod_instances).to eq([{
+ pod_name: 'Not provided',
+ stable: true,
+ status: 'pending',
+ tooltip: 'Not provided (Pending)',
+ track: 'stable'
+ }])
+ end
+
+ it 'returns instances when there are two stable deployments' do
+ deployments, pods = setup([
+ kube_deployment(name: 'one', track: 'stable', replicas: 1),
+ kube_deployment(name: 'two', track: 'stable', replicas: 1)
+ ], [
+ kube_pod(name: 'one', status: 'Running', track: 'stable'),
+ kube_pod(name: 'two', status: 'Running', track: 'stable')
+ ])
+ rollout_instances = described_class.new(deployments, pods)
+
+ expect(rollout_instances.pod_instances).to eq([{
+ pod_name: 'one',
+ stable: true,
+ status: 'running',
+ tooltip: 'one (Running)',
+ track: 'stable'
+ }, {
+ pod_name: 'two',
+ stable: true,
+ status: 'running',
+ tooltip: 'two (Running)',
+ track: 'stable'
+ }])
+ end
+
+ it 'returns instances for two deployments with different tracks' do
+ deployments, pods = setup([
+ kube_deployment(name: 'one', track: 'mytrack', replicas: 1),
+ kube_deployment(name: 'two', track: 'othertrack', replicas: 1)
+ ], [
+ kube_pod(name: 'one', status: 'Running', track: 'mytrack'),
+ kube_pod(name: 'two', status: 'Running', track: 'othertrack')
+ ])
+ rollout_instances = described_class.new(deployments, pods)
+
+ expect(rollout_instances.pod_instances).to eq([{
+ pod_name: 'one',
+ stable: false,
+ status: 'running',
+ tooltip: 'one (Running)',
+ track: 'mytrack'
+ }, {
+ pod_name: 'two',
+ stable: false,
+ status: 'running',
+ tooltip: 'two (Running)',
+ track: 'othertrack'
+ }])
+ end
+
+ it 'sorts stable tracks after canary tracks' do
+ deployments, pods = setup([
+ kube_deployment(name: 'one', track: 'stable', replicas: 1),
+ kube_deployment(name: 'two', track: 'canary', replicas: 1)
+ ], [
+ kube_pod(name: 'one', status: 'Running', track: 'stable'),
+ kube_pod(name: 'two', status: 'Running', track: 'canary')
+ ])
+ rollout_instances = described_class.new(deployments, pods)
+
+ expect(rollout_instances.pod_instances).to eq([{
+ pod_name: 'two',
+ stable: false,
+ status: 'running',
+ tooltip: 'two (Running)',
+ track: 'canary'
+ }, {
+ pod_name: 'one',
+ stable: true,
+ status: 'running',
+ tooltip: 'one (Running)',
+ track: 'stable'
+ }])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/rollout_status_spec.rb b/spec/lib/gitlab/kubernetes/rollout_status_spec.rb
new file mode 100644
index 00000000000..8ed9fdd799c
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/rollout_status_spec.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kubernetes::RolloutStatus do
+ include KubernetesHelpers
+
+ let(:track) { nil }
+ let(:specs) { specs_all_finished }
+
+ let(:pods) do
+ create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: "canary")
+ end
+
+ let(:ingresses) { [] }
+
+ let(:specs_all_finished) do
+ [
+ kube_deployment(name: 'one'),
+ kube_deployment(name: 'two', track: track)
+ ]
+ end
+
+ let(:specs_half_finished) do
+ [
+ kube_deployment(name: 'one'),
+ kube_deployment(name: 'two', track: track)
+ ]
+ end
+
+ subject(:rollout_status) { described_class.from_deployments(*specs, pods_attrs: pods, ingresses: ingresses) }
+
+ describe '#deployments' do
+ it 'stores the deployments' do
+ expect(rollout_status.deployments).to be_kind_of(Array)
+ expect(rollout_status.deployments.size).to eq(2)
+ expect(rollout_status.deployments.first).to be_kind_of(::Gitlab::Kubernetes::Deployment)
+ end
+ end
+
+ describe '#instances' do
+ context 'for stable track' do
+ let(:track) { "any" }
+
+ let(:pods) do
+ create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: "any")
+ end
+
+ it 'stores the union of deployment instances' do
+ expected = [
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'any', stable: false },
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'any', stable: false },
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'any', stable: false },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true }
+ ]
+
+ expect(rollout_status.instances).to eq(expected)
+ end
+ end
+
+ context 'for stable track' do
+ let(:track) { 'canary' }
+
+ let(:pods) do
+ create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: track)
+ end
+
+ it 'sorts stable instances last' do
+ expected = [
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'canary', stable: false },
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'canary', stable: false },
+ { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'canary', stable: false },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true },
+ { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true }
+ ]
+
+ expect(rollout_status.instances).to eq(expected)
+ end
+ end
+ end
+
+ describe '#completion' do
+ subject { rollout_status.completion }
+
+ context 'when all instances are finished' do
+ let(:track) { 'canary' }
+
+ it { is_expected.to eq(100) }
+ end
+
+ context 'when half of the instances are finished' do
+ let(:track) { "canary" }
+
+ let(:pods) do
+ create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: track, status: "Pending")
+ end
+
+ let(:specs) { specs_half_finished }
+
+ it { is_expected.to eq(50) }
+ end
+
+ context 'with one deployment' do
+ it 'sets the completion percentage when a deployment has more running pods than desired' do
+ deployments = [kube_deployment(name: 'one', track: 'one', replicas: 2)]
+ pods = create_pods(name: 'one', track: 'one', count: 3)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(100)
+ end
+ end
+
+ context 'with two deployments on different tracks' do
+ it 'sets the completion percentage when all pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'one', replicas: 2),
+ kube_deployment(name: 'two', track: 'two', replicas: 2)
+ ]
+ pods = create_pods(name: 'one', track: 'one', count: 2) + create_pods(name: 'two', track: 'two', count: 2)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(100)
+ end
+ end
+
+ context 'with two deployments that both have track set to "stable"' do
+ it 'sets the completion percentage when all pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 2),
+ kube_deployment(name: 'two', track: 'stable', replicas: 2)
+ ]
+ pods = create_pods(name: 'one', track: 'stable', count: 2) + create_pods(name: 'two', track: 'stable', count: 2)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(100)
+ end
+
+ it 'sets the completion percentage when no pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 3),
+ kube_deployment(name: 'two', track: 'stable', replicas: 7)
+ ]
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: [])
+
+ expect(rollout_status.completion).to eq(0)
+ end
+
+ it 'sets the completion percentage when a quarter of the pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 6),
+ kube_deployment(name: 'two', track: 'stable', replicas: 2)
+ ]
+ pods = create_pods(name: 'one', track: 'stable', count: 2)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(25)
+ end
+ end
+
+ context 'with two deployments, one with track set to "stable" and one with no track label' do
+ it 'sets the completion percentage when all pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 3),
+ kube_deployment(name: 'two', track: nil, replicas: 3)
+ ]
+ pods = create_pods(name: 'one', track: 'stable', count: 3) + create_pods(name: 'two', track: nil, count: 3)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(100)
+ end
+
+ it 'sets the completion percentage when no pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 1),
+ kube_deployment(name: 'two', track: nil, replicas: 1)
+ ]
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: [])
+
+ expect(rollout_status.completion).to eq(0)
+ end
+
+ it 'sets the completion percentage when a third of the pods are complete' do
+ deployments = [
+ kube_deployment(name: 'one', track: 'stable', replicas: 2),
+ kube_deployment(name: 'two', track: nil, replicas: 7)
+ ]
+ pods = create_pods(name: 'one', track: 'stable', count: 2) + create_pods(name: 'two', track: nil, count: 1)
+ rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods)
+
+ expect(rollout_status.completion).to eq(33)
+ end
+ end
+ end
+
+ describe '#complete?' do
+ subject { rollout_status.complete? }
+
+ context 'when all instances are finished' do
+ let(:track) { 'canary' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when half of the instances are finished' do
+ let(:track) { "canary" }
+
+ let(:pods) do
+ create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: track, status: "Pending")
+ end
+
+ let(:specs) { specs_half_finished }
+
+ it { is_expected.to be_falsy}
+ end
+ end
+
+ describe '#found?' do
+ context 'when the specs are passed' do
+ it { is_expected.to be_found }
+ end
+
+ context 'when list of specs is empty' do
+ let(:specs) { [] }
+
+ it { is_expected.not_to be_found }
+ end
+ end
+
+ describe '.loading' do
+ subject { described_class.loading }
+
+ it { is_expected.to be_loading }
+ end
+
+ describe '#not_found?' do
+ context 'when the specs are passed' do
+ it { is_expected.not_to be_not_found }
+ end
+
+ context 'when list of specs is empty' do
+ let(:specs) { [] }
+
+ it { is_expected.to be_not_found }
+ end
+ end
+
+ describe '#canary_ingress_exists?' do
+ context 'when canary ingress exists' do
+ let(:ingresses) { [kube_ingress(track: :canary)] }
+
+ it 'returns true' do
+ expect(rollout_status.canary_ingress_exists?).to eq(true)
+ end
+ end
+
+ context 'when canary ingress does not exist' do
+ let(:ingresses) { [kube_ingress(track: :stable)] }
+
+ it 'returns false' do
+ expect(rollout_status.canary_ingress_exists?).to eq(false)
+ end
+ end
+ end
+
+ def create_pods(name:, count:, track: nil, status: 'Running' )
+ Array.new(count, kube_pod(name: name, status: status, track: track))
+ end
+end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index e877ba2ac96..fb0613187c5 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
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) }
@@ -406,32 +407,62 @@ RSpec.describe Clusters::Platforms::Kubernetes do
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
@@ -451,7 +482,351 @@ RSpec.describe Clusters::Platforms::Kubernetes do
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
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 27c9e62712c..90884bfd0fb 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1421,4 +1421,123 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
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
diff --git a/spec/serializers/rollout_status_entity_spec.rb b/spec/serializers/rollout_status_entity_spec.rb
new file mode 100644
index 00000000000..7ad4b259bcd
--- /dev/null
+++ b/spec/serializers/rollout_status_entity_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe RolloutStatusEntity do
+ include KubernetesHelpers
+
+ let(:rollout_status) { kube_deployment_rollout_status }
+
+ let(:entity) do
+ described_class.new(rollout_status, request: double)
+ end
+
+ subject { entity.as_json }
+
+ it "exposes status" do
+ is_expected.to include(:status)
+ end
+
+ it 'exposes has_legacy_app_label' do
+ is_expected.to include(:has_legacy_app_label)
+ end
+
+ context 'when kube deployment is valid' do
+ it "exposes deployment data" do
+ is_expected.to include(:instances, :completion, :is_completed)
+ end
+
+ it 'does not expose canary ingress if it does not exist' do
+ is_expected.not_to include(:canary_ingress)
+ end
+
+ context 'when canary ingress exists' do
+ let(:rollout_status) { kube_deployment_rollout_status(ingresses: [kube_ingress(track: :canary)]) }
+
+ it 'expose canary ingress' do
+ is_expected.to include(:canary_ingress)
+ end
+ end
+ end
+
+ context 'when kube deployment is empty' do
+ let(:rollout_status) { empty_deployment_rollout_status }
+
+ it "exposes status" do
+ is_expected.to include(:status)
+ end
+
+ it "does not expose deployment data" do
+ is_expected.not_to include(:instances, :completion, :is_completed, :canary_ingress)
+ end
+ end
+end
diff --git a/spec/serializers/rollout_statuses/ingress_entity_spec.rb b/spec/serializers/rollout_statuses/ingress_entity_spec.rb
new file mode 100644
index 00000000000..b87b9e5c6c4
--- /dev/null
+++ b/spec/serializers/rollout_statuses/ingress_entity_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe RolloutStatuses::IngressEntity do
+ include KubernetesHelpers
+
+ let(:canary_ingress) { kube_ingress(track: :canary) }
+
+ let(:entity) do
+ described_class.new(canary_ingress, request: double)
+ end
+
+ subject { entity.as_json }
+
+ it 'exposes canary weight' do
+ is_expected.to include(:canary_weight)
+ end
+end