Commit c67abeaa authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'canary-deployments' into 'master'

Read track from deployment and visualise canary deployments

See merge request !1551
parents 644abaf2 48a551e8
...@@ -163,7 +163,8 @@ export default { ...@@ -163,7 +163,8 @@ export default {
<template v-for="instance in deployBoardData.instances"> <template v-for="instance in deployBoardData.instances">
<instance-component <instance-component
:status="instance.status" :status="instance.status"
:tooltipText="instance.tooltip"/> :tooltip-text="instance.tooltip"
:stable="instance.stable" />
</template> </template>
</div> </div>
</section> </section>
......
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
* see more information about this in * see more information about this in
* https://gitlab.com/gitlab-org/gitlab-ee/uploads/5fff049fd88336d9ee0c6ef77b1ba7e3/monitoring__deployboard--key.png * https://gitlab.com/gitlab-org/gitlab-ee/uploads/5fff049fd88336d9ee0c6ef77b1ba7e3/monitoring__deployboard--key.png
* *
* An instance can represent a normal deploy or a canary deploy. In the latter we need to provide
* this information in the tooltip and the colors.
* Mockup is https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1551#note_26595150
*/ */
export default { export default {
...@@ -28,11 +31,23 @@ export default { ...@@ -28,11 +31,23 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
stable: {
type: Boolean,
required: false,
default: true,
},
}, },
computed: { computed: {
cssClass() { cssClass() {
return `deploy-board-instance-${this.status}`; let cssClassName = `deploy-board-instance-${this.status}`;
if (!this.stable) {
cssClassName = `${cssClassName} deploy-board-instance-canary`;
}
return cssClassName;
}, },
}, },
......
...@@ -240,6 +240,9 @@ ...@@ -240,6 +240,9 @@
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
margin: 1px; margin: 1px;
display: flex;
justify-content: center;
align-items: center;
&-finished { &-finished {
background-color: $green-100; background-color: $green-100;
...@@ -270,6 +273,17 @@ ...@@ -270,6 +273,17 @@
background-color: $white-light; background-color: $white-light;
border-color: $border-color; border-color: $border-color;
} }
&.deploy-board-instance-canary {
&::after {
width: 7px;
height: 7px;
border: 1px solid $white-light;
background-color: $orange-300;
border-radius: 50%;
content: "";
}
}
} }
.deploy-board-icon i { .deploy-board-icon i {
......
...@@ -116,6 +116,8 @@ class Project < ActiveRecord::Base ...@@ -116,6 +116,8 @@ class Project < ActiveRecord::Base
has_one :prometheus_service, dependent: :destroy, inverse_of: :project has_one :prometheus_service, dependent: :destroy, inverse_of: :project
has_one :index_status, dependent: :destroy has_one :index_status, dependent: :destroy
has_one :mock_ci_service, dependent: :destroy has_one :mock_ci_service, dependent: :destroy
has_one :mock_deployment_service, dependent: :destroy
has_one :mock_monitoring_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link has_one :forked_from_project, through: :forked_project_link
......
class MockDeploymentService < DeploymentService
def title
'Mock deployment'
end
def description
'Mock deployment service'
end
def self.to_param
'mock_deployment'
end
# No terminals support
def terminals(environment)
[]
end
def rollout_status(environment)
OpenStruct.new(
instances: rollout_status_instances,
completion: 80,
valid?: true,
complete?: true
)
end
private
def rollout_status_instances
JSON.parse(Rails.root.join('spec', 'fixtures', 'rollout_status_instances.json'))
end
end
class MockMonitoringService < MonitoringService
def title
'Mock monitoring'
end
def description
'Mock monitoring service'
end
def self.to_param
'mock_monitoring'
end
def metrics(environment)
JSON.parse(Rails.root.join('spec', 'fixtures', 'metrics.json'))
end
end
...@@ -240,7 +240,9 @@ class Service < ActiveRecord::Base ...@@ -240,7 +240,9 @@ class Service < ActiveRecord::Base
slack slack
teamcity teamcity
] ]
service_names << 'mock_ci' if Rails.env.development? if Rails.env.development?
service_names += %w[mock_ci mock_deployment mock_monitoring]
end
service_names.sort_by(&:downcase) service_names.sort_by(&:downcase)
end end
......
---
title: Added mock data for Deployboard
merge_request:
author:
---
title: Visualise Canary Deployments
merge_request:
author:
---
title: Added mock deployment and monitoring service with environments fixtures
merge_request:
author:
require './spec/support/sidekiq'
class Gitlab::Seeder::Environments
def initialize(project)
@project = project
end
def seed!
@project.create_mock_deployment_service!(active: true)
@project.create_mock_monitoring_service!(active: true)
create_master_deployments!('production')
create_master_deployments!('staging')
create_merge_request_review_deployments!
end
private
def create_master_deployments!(name)
@project.repository.commits('master', limit: 4).map do |commit|
create_deployment!(
@project,
name,
'master',
commit.id
)
end
end
def create_merge_request_review_deployments!
@project.merge_requests.sample(4).map do |merge_request|
next unless merge_request.diff_head_sha
create_deployment!(
merge_request.source_project,
"review/#{merge_request.source_branch}",
merge_request.source_branch,
merge_request.diff_head_sha
)
end
end
def create_deployment!(project, name, ref, sha)
environment = find_or_create_environment!(project, name)
environment.deployments.create!(
project: project,
ref: ref,
sha: sha,
tag: false,
deployable: find_deployable(project, name)
)
end
def find_or_create_environment!(project, name)
project.environments.find_or_create_by!(name: name).tap do |environment|
environment.update(external_url: "https://google.com/#{name}")
end
end
def find_deployable(project, environment)
project.builds.where(environment: environment).sample
end
end
Gitlab::Seeder.quiet do
Project.all.sample(5).each do |project|
project_environments = Gitlab::Seeder::Environments.new(project)
project_environments.seed!
end
end
...@@ -610,8 +610,14 @@ module API ...@@ -610,8 +610,14 @@ module API
desc: 'URL to the mock service' desc: 'URL to the mock service'
} }
] ]
services['mock-deployment'] = []
services['mock-monitoring'] = []
service_classes << MockCiService service_classes += [
MockCiService,
MockDeploymentService,
MockMonitoringService,
]
end end
trigger_services = { trigger_services = {
......
...@@ -10,7 +10,15 @@ module Gitlab ...@@ -10,7 +10,15 @@ module Gitlab
end end
def labels def labels
metadata['labels'] metadata.fetch('labels', {})
end
def track
labels.fetch('track', 'stable')
end
def stable?
track == 'stable'
end end
def outdated? def outdated?
...@@ -52,7 +60,12 @@ module Gitlab ...@@ -52,7 +60,12 @@ module Gitlab
end end
def deployment_instance(n, name, status) def deployment_instance(n, name, status)
{ status: status, tooltip: "#{name} (pod #{n}) #{status.capitalize}" } {
status: status,
tooltip: "#{name} (pod #{n}) #{status.capitalize}",
track: track,
stable: stable?
}
end end
def metadata def metadata
......
This diff is collapsed.
[
{
"status": "finished",
"tooltip": "production (pod 0) Finished",
"track": "stable",
"stable": true
},
{
"status": "deploying",
"tooltip": "production (pod 1) Deploying",
"track": "stable",
"stable": true
},
{
"status": "failed",
"tooltip": "production (pod 2) Failed",
"track": "stable",
"stable": true
},
{
"status": "ready",
"tooltip": "production (pod 3) Ready",
"track": "stable",
"stable": true
},
{
"status": "preparing",
"tooltip": "production (pod 4) Preparing",
"track": "stable",
"stable": true
},
{
"status": "waiting",
"tooltip": "production (pod 5) Waiting",
"track": "stable",
"stable": true
},
{
"status": "finished",
"tooltip": "production-canary (pod 0) Finished",
"track": "canary",
"stable": false
},
{
"status": "deploying",
"tooltip": "production-canary (pod 1) Deploying",
"track": "canary",
"stable": false
},
{
"status": "failed",
"tooltip": "production-canary (pod 2) Failed",
"track": "canary",
"stable": false
},
{
"status": "ready",
"tooltip": "production-canary (pod 3) Ready",
"track": "canary",
"stable": false
},
{
"status": "preparing",
"tooltip": "production-canary (pod 4) Preparing",
"track": "canary",
"stable": false
},
{
"status": "waiting",
"tooltip": "production-canary (pod 5) Waiting",
"track": "canary",
"stable": false
}
]
...@@ -30,4 +30,15 @@ describe('Deploy Board Instance', () => { ...@@ -30,4 +30,15 @@ describe('Deploy Board Instance', () => {
expect(component.$el.classList.contains('deploy-board-instance-deploying')).toBe(true); expect(component.$el.classList.contains('deploy-board-instance-deploying')).toBe(true);
expect(component.$el.getAttribute('data-title')).toEqual(''); expect(component.$el.getAttribute('data-title')).toEqual('');
}); });
it('should render a div with canary class when stable prop is provided as false', () => {
const component = new DeployBoardInstanceComponent({
propsData: {
status: 'deploying',
stable: false,
},
}).$mount();
expect(component.$el.classList.contains('deploy-board-instance-canary')).toBe(true);
});
}); });
...@@ -164,6 +164,8 @@ project: ...@@ -164,6 +164,8 @@ project:
- external_wiki_service - external_wiki_service
- kubernetes_service - kubernetes_service
- mock_ci_service - mock_ci_service
- mock_deployment_service
- mock_monitoring_service
- forked_project_link - forked_project_link
- forked_from_project - forked_from_project
- forked_project_links - forked_project_links
......
...@@ -5,60 +5,70 @@ describe Gitlab::Kubernetes::Deployment do ...@@ -5,60 +5,70 @@ describe Gitlab::Kubernetes::Deployment do
describe '#name' do describe '#name' do
let(:params) { named(:selected) } let(:params) { named(:selected) }
it { expect(deployment.name).to eq(:selected) } it { expect(deployment.name).to eq(:selected) }
end end
describe '#labels' do describe '#labels' do
let(:params) { make('metadata', 'labels' => :selected) } let(:params) { make('metadata', 'labels' => :selected) }
it { expect(deployment.labels).to eq(:selected) } it { expect(deployment.labels).to eq(:selected) }
end end
describe '#outdated?' do describe '#outdated?' do
context 'when outdated' do context 'when outdated' do
let(:params) { generation(2, 1) } let(:params) { generation(2, 1) }
it { expect(deployment.outdated?).to be_truthy } it { expect(deployment.outdated?).to be_truthy }
end end
context 'when up to date' do context 'when up to date' do
let(:params) { generation(2, 2) } let(:params) { generation(2, 2) }
it { expect(deployment.outdated?).to be_falsy } it { expect(deployment.outdated?).to be_falsy }
end end
context 'when ahead of latest' do context 'when ahead of latest' do
let(:params) { generation(1, 2) } let(:params) { generation(1, 2) }
it { expect(deployment.outdated?).to be_falsy } it { expect(deployment.outdated?).to be_falsy }
end end
end end
describe '#wanted_replicas' do describe '#wanted_replicas' do
let(:params) { make('spec', 'replicas' => :selected ) } let(:params) { make('spec', 'replicas' => :selected ) }
it { expect(deployment.wanted_replicas).to eq(:selected) } it { expect(deployment.wanted_replicas).to eq(:selected) }
end end
describe '#finished_replicas' do describe '#finished_replicas' do
let(:params) { make('status', 'availableReplicas' => :selected) } let(:params) { make('status', 'availableReplicas' => :selected) }
it { expect(deployment.finished_replicas).to eq(:selected) } it { expect(deployment.finished_replicas).to eq(:selected) }
end end
describe '#deploying_replicas' do describe '#deploying_replicas' do
let(:params) { make('status', 'availableReplicas' => 2, 'updatedReplicas' => 4) } let(:params) { make('status', 'availableReplicas' => 2, 'updatedReplicas' => 4) }
it { expect(deployment.deploying_replicas).to eq(2) } it { expect(deployment.deploying_replicas).to eq(2) }
end end
describe '#waiting_replicas' do describe '#waiting_replicas' do
let(:params) { combine(make('spec', 'replicas' => 4), make('status', 'updatedReplicas' => 2)) } let(:params) { combine(make('spec', 'replicas' => 4), make('status', 'updatedReplicas' => 2)) }
it { expect(deployment.waiting_replicas).to eq(2) } it { expect(deployment.waiting_replicas).to eq(2) }
end end
describe '#instances' do describe '#instances' do
context 'when unnamed' do context 'when unnamed' do
let(:params) { combine(generation(1, 1), instances) } let(:params) { combine(generation(1, 1), instances) }
it 'returns all instances as unknown and waiting' do it 'returns all instances as unknown and waiting' do
expected = [ expected = [
{ status: 'waiting', tooltip: 'unknown (pod 0) Waiting' }, { status: 'waiting', tooltip: 'unknown (pod 0) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'unknown (pod 1) Waiting' }, { status: 'waiting', tooltip: 'unknown (pod 1) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'unknown (pod 2) Waiting' }, { status: 'waiting', tooltip: 'unknown (pod 2) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'unknown (pod 3) Waiting' }, { status: 'waiting', tooltip: 'unknown (pod 3) Waiting', track: 'stable', stable: true },
] ]
expect(deployment.instances).to eq(expected) expect(deployment.instances).to eq(expected)
...@@ -67,12 +77,13 @@ describe Gitlab::Kubernetes::Deployment do ...@@ -67,12 +77,13 @@ describe Gitlab::Kubernetes::Deployment do
context 'when outdated' do context 'when outdated' do
let(:params) { combine(named('foo'), generation(1, 0), instances) } let(:params) { combine(named('foo'), generation(1, 0), instances) }
it 'returns all instances as named and waiting' do it 'returns all instances as named and waiting' do
expected = [ expected = [
{ status: 'waiting', tooltip: 'foo (pod 0) Waiting' }, { status: 'waiting', tooltip: 'foo (pod 0) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'foo (pod 1) Waiting' }, { status: 'waiting', tooltip: 'foo (pod 1) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'foo (pod 2) Waiting' }, { status: 'waiting', tooltip: 'foo (pod 2) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'foo (pod 3) Waiting' }, { status: 'waiting', tooltip: 'foo (pod 3) Waiting', track: 'stable', stable: true },
] ]
expect(deployment.instances).to eq(expected) expect(deployment.instances).to eq(expected)
...@@ -84,15 +95,44 @@ describe Gitlab::Kubernetes::Deployment do ...@@ -84,15 +95,44 @@ describe Gitlab::Kubernetes::Deployment do
it 'returns all instances' do it 'returns all instances' do
expected = [ expected = [
{ status: 'finished', tooltip: 'foo (pod 0) Finished' }, { status: 'finished', tooltip: 'foo (pod 0) Finished', track: 'stable', stable: true },
{ status: 'deploying', tooltip: 'foo (pod 1) Deploying' }, { status: 'deploying', tooltip: 'foo (pod 1) Deploying', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'foo (pod 2) Waiting' }, { status: 'waiting', tooltip: 'foo (pod 2) Waiting', track: 'stable', stable: true },
{ status: 'waiting', tooltip: 'foo (pod 3) Waiting' }, { status: 'waiting', tooltip: 'foo (pod 3) Waiting', track: 'stable', stable: true },
] ]
expect(deployment.instances).to eq(expected) expect(deployment.instances).to eq(expected)
end end
end end
context 'with track label' do
let(:labels) { { 'track' => track } }
let(:params) { combine(named('foo', labels), generation(1, 0), instances(1, 1, 1, labels)) }
context 'when marked as stable' do
let(:track) { 'stable' }
it 'returns all instances' do
expected = [
{ status: 'waiting', tooltip: 'foo (pod 0) Waiting', track: 'stable', stable: true },
]
expect(deployment.instances).to eq(expected)
end
end
context 'when marked as canary' do
let(:track) { 'canary' }
it 'returns all instances' do
expected = [
{ status: 'waiting', tooltip: 'foo (pod 0) Waiting', track: 'canary', stable: false },
]
expect(deployment.instances).to eq(expected)
end
end
end
end end
def generation(expected, observed) def generation(expected, observed)
...@@ -102,14 +142,14 @@ describe Gitlab::Kubernetes::Deployment do ...@@ -102,14 +142,14 @@ describe Gitlab::Kubernetes::Deployment do
) )
end end
def named(name = "foo") def named(name = "foo", labels = {})
make('metadata', 'name' => name) make('metadata', 'name' => name, 'labels' => labels)
end end
def instances def instances(replicas = 4, available = 1, updated = 2, labels = {})
combine( combine(
make('spec', 'replicas' => 4), make('spec', 'replicas' => replicas),
make('status', 'availableReplicas' => 1, 'updatedReplicas' => 2), make('status', 'availableReplicas' => available, 'updatedReplicas' => updated),
) )
end end
......
...@@ -2,16 +2,25 @@ require 'spec_helper' ...@@ -2,16 +2,25 @@ require 'spec_helper'
describe Gitlab::Kubernetes::RolloutStatus do describe Gitlab::Kubernetes::RolloutStatus do
include KubernetesHelpers include KubernetesHelpers
let(:specs_all_finished) { [kube_deployment(name: 'one'), kube_deployment(name: 'two')] }
let(:specs_half_finished) do let(:track) { nil }
let(:specs) { specs_all_finished }
let(:specs_none) { [] }
let(:specs_all_finished) do
[ [
kube_deployment(name: 'one'), kube_deployment(name: 'one'),
kube_deployment(name: 'two').deep_merge('status' => { 'availableReplicas' => 0 }) kube_deployment(name: 'two', track: track)
] ]
end end
let(:specs) { specs_all_finished } let(:specs_half_finished) do
let(:specs_none) { [] } [
kube_deployment(name: 'one'),
kube_deployment(name: 'two', track: track)
.deep_merge('status' => { 'availableReplicas' => 0 })
]
end
subject(:rollout_status) { described_class.from_specs(*specs) } subject(:rollout_status) { described_class.from_specs(*specs) }
...@@ -24,20 +33,39 @@ describe Gitlab::Kubernetes::RolloutStatus do ...@@ -24,20 +33,39 @@ describe Gitlab::Kubernetes::RolloutStatus do
end end
describe '#instances' do describe '#instances' do
context 'for stable track' do
it 'stores the union of deployment instances' do it 'stores the union of deployment instances' do
expected = [ expected = [
{ status: 'finished', tooltip: 'one (pod 0) Finished' }, { status: 'finished', tooltip: 'one (pod 0) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 1) Finished' }, { status: 'finished', tooltip: 'one (pod 1) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 2) Finished' }, { status: 'finished', tooltip: 'one (pod 2) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'two (pod 0) Finished' }, { status: 'finished', tooltip: 'two (pod 0) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'two (pod 1) Finished' }, { status: 'finished', tooltip: 'two (pod 1) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'two (pod 2) Finished' }, { status: 'finished', tooltip: 'two (pod 2) Finished', track: 'stable', stable: true },
] ]
expect(rollout_status.instances).to eq(expected) expect(rollout_status.instances).to eq(expected)
end end
end end
context 'for stable track' do
let(:track) { 'canary' }
it 'stores the union of deployment instances' do
expected = [
{ status: 'finished', tooltip: 'one (pod 0) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 1) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'one (pod 2) Finished', track: 'stable', stable: true },
{ status: 'finished', tooltip: 'two (pod 0) Finished', track: 'canary', stable: false },
{ status: 'finished', tooltip: 'two (pod 1) Finished', track: 'canary', stable: false },
{ status: 'finished', tooltip: 'two (pod 2) Finished', track: 'canary', stable: false },
]
expect(rollout_status.instances).to eq(expected)
end
end
end
describe '#completion' do describe '#completion' do
subject { rollout_status.completion } subject { rollout_status.completion }
...@@ -47,6 +75,7 @@ describe Gitlab::Kubernetes::RolloutStatus do ...@@ -47,6 +75,7 @@ describe Gitlab::Kubernetes::RolloutStatus do
context 'when half of the instances are finished' do context 'when half of the instances are finished' do
let(:specs) { specs_half_finished } let(:specs) { specs_half_finished }
it { is_expected.to eq(50) } it { is_expected.to eq(50) }
end end
end end
...@@ -60,6 +89,7 @@ describe Gitlab::Kubernetes::RolloutStatus do ...@@ -60,6 +89,7 @@ describe Gitlab::Kubernetes::RolloutStatus do
context 'when half of the instances are finished' do context 'when half of the instances are finished' do
let(:specs) { specs_half_finished } let(:specs) { specs_half_finished }
it { is_expected.to be_falsy} it { is_expected.to be_falsy}
end end
end end
......
...@@ -85,12 +85,15 @@ module KubernetesHelpers ...@@ -85,12 +85,15 @@ module KubernetesHelpers
} }
end end
def kube_deployment(name: "kube-deployment", app: "valid-deployment-label") def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil)
{ {
"metadata" => { "metadata" => {
"name" => name, "name" => name,
"generation" => 4, "generation" => 4,
"labels" => { "app" => app }, "labels" => {
"app" => app,
"track" => track
}.compact,
}, },
"spec" => { "replicas" => 3 }, "spec" => { "replicas" => 3 },
"status" => { "status" => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment