Commit 44b07ee2 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch '3328-one-request-deploy-boards' into 'master'

Resolve "Improve visibility of deploy boards"

Closes #3328

See merge request gitlab-org/gitlab-ee!2984
parents 45d76e75 f5340667
...@@ -8,7 +8,9 @@ ...@@ -8,7 +8,9 @@
* - Button Actions. * - Button Actions.
* [Mockup](https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png) * [Mockup](https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png)
*/ */
import _ from 'underscore';
import deployBoardSvg from 'empty_states/icons/_deploy_board.svg'; import deployBoardSvg from 'empty_states/icons/_deploy_board.svg';
import { n__ } from '../../locale';
import instanceComponent from './deploy_board_instance_component.vue'; import instanceComponent from './deploy_board_instance_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
...@@ -26,40 +28,27 @@ ...@@ -26,40 +28,27 @@
type: Boolean, type: Boolean,
required: true, required: true,
}, },
hasError: { isEmpty: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
}, },
data() {
return {
deployBoardSvg,
};
},
computed: { computed: {
canRenderDeployBoard() { canRenderDeployBoard() {
return !this.isLoading && !this.hasError && this.deployBoardData.valid; return !this.isLoading && !this.isEmpty && !_.isEmpty(this.deployBoardData);
}, },
canRenderEmptyState() { canRenderEmptyState() {
return !this.isLoading && !this.hasError && !this.deployBoardData.valid; return !this.isLoading && this.isEmpty;
},
canRenderErrorState() {
return !this.isLoading && this.hasError;
}, },
instanceTitle() { instanceTitle() {
let title; return n__('Instance', 'Instances', this.deployBoardData.instances.length);
if (this.deployBoardData.instances.length === 1) {
title = 'Instance';
} else {
title = 'Instances';
}
return title;
}, },
projectName() { projectName() {
return '<projectname>'; return '<projectname>';
}, },
deployBoardSvg() {
return deployBoardSvg;
},
}, },
}; };
</script> </script>
...@@ -128,11 +117,5 @@ ...@@ -128,11 +117,5 @@
</span> </span>
</section> </section>
</div> </div>
<div
v-if="canRenderErrorState"
class="deploy-board-error-message">
We can't fetch the data right now. Please try again later.
</div>
</div> </div>
</script> </script>
...@@ -127,16 +127,10 @@ export default { ...@@ -127,16 +127,10 @@ export default {
/** /**
* Toggles the visibility of the deploy boards of the clicked environment. * Toggles the visibility of the deploy boards of the clicked environment.
*
* @param {Object} model * @param {Object} model
* @return {Object}
*/ */
toggleDeployBoard(model) { toggleDeployBoard(model) {
this.store.toggleDeployBoard(model.id); this.store.toggleDeployBoard(model.id);
if (!model.isDeployboardVisible) {
this.fetchDeployBoard(model, true);
}
}, },
toggleFolder(folder) { toggleFolder(folder) {
...@@ -203,11 +197,6 @@ export default { ...@@ -203,11 +197,6 @@ export default {
if (openFolders.length) { if (openFolders.length) {
openFolders.forEach(folder => this.fetchChildEnvironments(folder)); openFolders.forEach(folder => this.fetchChildEnvironments(folder));
} }
const openDeployBoards = this.store.getOpenDeployBoards();
if (openDeployBoards.length) {
openDeployBoards.forEach(env => this.fetchDeployBoard(env));
}
}, },
errorCallback() { errorCallback() {
...@@ -215,23 +204,6 @@ export default { ...@@ -215,23 +204,6 @@ export default {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.'); new Flash('An error occurred while fetching the environments.');
}, },
fetchDeployBoard(environment, showLoader = false) {
this.store.updateEnvironmentProp(environment, 'isLoadingDeployBoard', showLoader);
this.service.getDeployBoard(environment.rollout_status_path)
.then(resp => resp.json())
.then((data) => {
this.store.storeDeployBoard(environment.id, data);
this.store.updateEnvironmentProp(environment, 'isLoadingDeployBoard', false);
})
.catch(() => {
this.store.updateEnvironmentProp(environment, 'isLoadingDeployBoard', false);
this.store.updateEnvironmentProp(environment, 'hasErrorDeployBoard', true);
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the deploy board.');
});
},
}, },
}; };
</script> </script>
......
...@@ -74,7 +74,7 @@ export default { ...@@ -74,7 +74,7 @@ export default {
<deploy-board <deploy-board
:deploy-board-data="model.deployBoardData" :deploy-board-data="model.deployBoardData"
:is-loading="model.isLoadingDeployBoard" :is-loading="model.isLoadingDeployBoard"
:has-error="model.hasErrorDeployBoard" :is-empty="model.isEmptyDeployBoard"
/> />
</div> </div>
</div> </div>
......
...@@ -30,7 +30,7 @@ export default class EnvironmentsStore { ...@@ -30,7 +30,7 @@ export default class EnvironmentsStore {
* If the `size` is bigger than 1, it means it should be rendered as a folder. * If the `size` is bigger than 1, it means it should be rendered as a folder.
* In those cases we add `isFolder` key in order to render it properly. * In those cases we add `isFolder` key in order to render it properly.
* *
* Top level environments - when the size is 1 - with `rollout_status_path` * Top level environments - when the size is 1 - with `rollout_status`
* can render a deploy board. We add `isDeployBoardVisible` and `deployBoardData` * can render a deploy board. We add `isDeployBoardVisible` and `deployBoardData`
* keys to those environments. * keys to those environments.
* The first key will let's us know if we should or not render the deploy board. * The first key will let's us know if we should or not render the deploy board.
...@@ -65,13 +65,15 @@ export default class EnvironmentsStore { ...@@ -65,13 +65,15 @@ export default class EnvironmentsStore {
filtered = Object.assign(filtered, env); filtered = Object.assign(filtered, env);
} }
if (filtered.size === 1 && filtered.rollout_status_path) { if (filtered.size === 1 && filtered.rollout_status) {
filtered = Object.assign({}, filtered, { filtered = Object.assign({}, filtered, {
hasDeployBoard: true, hasDeployBoard: true,
isDeployBoardVisible: oldEnvironmentState.isDeployBoardVisible || false, isDeployBoardVisible: oldEnvironmentState.isDeployBoardVisible === false ?
deployBoardData: oldEnvironmentState.deployBoardData || {}, oldEnvironmentState.isDeployBoardVisible :
isLoadingDeployBoard: oldEnvironmentState.isLoadingDeployBoard || false, true,
hasErrorDeployBoard: oldEnvironmentState.hasErrorDeployBoard || false, deployBoardData: filtered.rollout_status.status === 'found' ? filtered.rollout_status : {},
isLoadingDeployBoard: filtered.rollout_status.status === 'loading',
isEmptyDeployBoard: filtered.rollout_status.status === 'not_found',
}); });
} }
return filtered; return filtered;
......
class Projects::EnvironmentsController < Projects::ApplicationController class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project' layout 'project'
before_action :authorize_read_environment! before_action :authorize_read_environment!
before_action :authorize_read_deploy_board!, only: :status
before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_create_deployment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update] before_action :authorize_update_environment!, only: [:edit, :update]
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics, :status] before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
def index def index
@environments = project.environments @environments = project.environments
...@@ -132,25 +132,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -132,25 +132,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
end end
# The rollout status of an enviroment
def status
unless @environment.deployment_service_ready?
render text: 'Not found', status: 404
return
end
rollout_status = @environment.rollout_status
Gitlab::PollingInterval.set_header(response, interval: 3000) unless rollout_status.try(:complete?)
if rollout_status.nil?
render body: nil, status: 204 # no result yet
else
serializer = RolloutStatusSerializer.new(project: @project, current_user: @current_user)
render json: serializer.represent(rollout_status)
end
end
def additional_metrics def additional_metrics
respond_to do |format| respond_to do |format|
format.json do format.json do
...@@ -167,6 +148,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -167,6 +148,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
Gitlab::Workhorse.verify_api_request!(request.headers) Gitlab::Workhorse.verify_api_request!(request.headers)
end end
def expire_etag_cache
return if request.format.json?
# this forces to reload json content
Gitlab::EtagCaching::Store.new.tap do |store|
store.touch(project_environments_path(project, format: :json))
end
end
def environment_params def environment_params
params.require(:environment).permit(:name, :external_url) params.require(:environment).permit(:name, :external_url)
end end
......
...@@ -59,6 +59,9 @@ module ReactiveCaching ...@@ -59,6 +59,9 @@ module ReactiveCaching
raise NotImplementedError raise NotImplementedError
end end
def reactive_cache_updated(*args)
end
def with_reactive_cache(*args, &blk) def with_reactive_cache(*args, &blk)
within_reactive_cache_lifetime(*args) do within_reactive_cache_lifetime(*args) do
data = Rails.cache.read(full_reactive_cache_key(*args)) data = Rails.cache.read(full_reactive_cache_key(*args))
...@@ -77,8 +80,11 @@ module ReactiveCaching ...@@ -77,8 +80,11 @@ module ReactiveCaching
locking_reactive_cache(*args) do locking_reactive_cache(*args) do
within_reactive_cache_lifetime(*args) do within_reactive_cache_lifetime(*args) do
enqueuing_update(*args) do enqueuing_update(*args) do
value = calculate_reactive_cache(*args) key = full_reactive_cache_key(*args)
Rails.cache.write(full_reactive_cache_key(*args), value) new_value = calculate_reactive_cache(*args)
old_value = Rails.cache.read(key)
Rails.cache.write(key, new_value)
reactive_cache_updated(*args) if new_value != old_value
end end
end end
end end
......
...@@ -17,12 +17,14 @@ class MockDeploymentService < DeploymentService ...@@ -17,12 +17,14 @@ class MockDeploymentService < DeploymentService
end end
def rollout_status(environment) def rollout_status(environment)
OpenStruct.new( case environment.name
instances: rollout_status_instances, when 'staging'
completion: 80, Gitlab::Kubernetes::RolloutStatus.new([], status: :not_found)
valid?: true, when 'test'
complete?: true Gitlab::Kubernetes::RolloutStatus.new([], status: :loading)
) else
Gitlab::Kubernetes::RolloutStatus.new(rollout_status_deployments)
end
end end
private private
...@@ -31,4 +33,8 @@ class MockDeploymentService < DeploymentService ...@@ -31,4 +33,8 @@ class MockDeploymentService < DeploymentService
data = File.read(Rails.root.join('spec', 'fixtures', 'rollout_status_instances.json')) data = File.read(Rails.root.join('spec', 'fixtures', 'rollout_status_instances.json'))
JSON.parse(data) JSON.parse(data)
end end
def rollout_status_deployments
[OpenStruct.new(instances: rollout_status_instances)]
end
end end
...@@ -9,7 +9,9 @@ class EnvironmentEntity < Grape::Entity ...@@ -9,7 +9,9 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity expose :last_deployment, using: DeploymentEntity
expose :stop_action? expose :stop_action?
expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment| expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity
expose :metrics_path, if: -> (*) { environment.has_metrics? } do |environment|
metrics_project_environment_path(environment.project, environment) metrics_project_environment_path(environment.project, environment)
end end
...@@ -21,19 +23,26 @@ class EnvironmentEntity < Grape::Entity ...@@ -21,19 +23,26 @@ class EnvironmentEntity < Grape::Entity
stop_project_environment_path(environment.project, environment) stop_project_environment_path(environment.project, environment)
end end
expose :terminal_path, if: ->(environment, _) { environment.deployment_service_ready? } do |environment| expose :terminal_path, if: ->(*) { environment.deployment_service_ready? } do |environment|
can?(request.current_user, :admin_environment, environment.project) && can?(request.current_user, :admin_environment, environment.project) &&
terminal_project_environment_path(environment.project, environment) terminal_project_environment_path(environment.project, environment)
end end
expose :rollout_status_path, if: ->(environment, _) { environment.deployment_service_ready? } do |environment|
can?(request.current_user, :read_deploy_board, environment.project) &&
status_project_environment_path(environment.project, environment, format: :json)
end
expose :folder_path do |environment| expose :folder_path do |environment|
folder_project_environments_path(environment.project, environment.folder_name) folder_project_environments_path(environment.project, environment.folder_name)
end end
expose :created_at, :updated_at expose :created_at, :updated_at
private
alias_method :environment, :object
def current_user
request.current_user
end
def can_read_deploy_board?
can?(current_user, :read_deploy_board, environment.project)
end
end end
class RolloutStatusEntity < Grape::Entity class RolloutStatusEntity < Grape::Entity
include RequestAwareEntity include RequestAwareEntity
expose :instances expose :status, as: :status
expose :completion
expose :valid?, as: :valid
expose :is_completed do |rollout_status| expose :instances, if: -> (rollout_status, _) { rollout_status.found? }
rollout_status.complete? expose :completion, if: -> (rollout_status, _) { rollout_status.found? }
end expose :complete?, as: :is_completed, if: -> (rollout_status, _) { rollout_status.found? }
end end
class RolloutStatusSerializer < BaseSerializer
entity RolloutStatusEntity
end
---
title: Improves visibility of deploy boards
merge_request:
author:
type: changed
...@@ -224,7 +224,6 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -224,7 +224,6 @@ constraints(ProjectUrlConstrainer.new) do
get :terminal get :terminal
get :metrics get :metrics
get :additional_metrics get :additional_metrics
get :status, constraints: { format: :json }
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
end end
......
module EE module EE
module KubernetesService module KubernetesService
def rollout_status(environment) def rollout_status(environment)
with_reactive_cache do |data| result = with_reactive_cache do |data|
specs = filter_by_label(data[:deployments], app: environment.slug) specs = filter_by_label(data[:deployments], app: environment.slug)
::Gitlab::Kubernetes::RolloutStatus.from_specs(*specs) ::Gitlab::Kubernetes::RolloutStatus.from_specs(*specs)
end end
result || ::Gitlab::Kubernetes::RolloutStatus.loading
end end
def calculate_reactive_cache def calculate_reactive_cache
...@@ -15,6 +16,15 @@ module EE ...@@ -15,6 +16,15 @@ module EE
result result
end end
def reactive_cache_updated
super
::Gitlab::EtagCaching::Store.new.tap do |store|
store.touch(
::Gitlab::Routing.url_helpers.project_environments_path(project, format: :json))
end
end
def read_deployments def read_deployments
kubeclient = build_kubeclient!(api_path: 'apis/extensions', api_version: 'v1beta1') kubeclient = build_kubeclient!(api_path: 'apis/extensions', api_version: 'v1beta1')
......
...@@ -6,26 +6,38 @@ module Gitlab ...@@ -6,26 +6,38 @@ module Gitlab
# other resources, unified by an `app=` label. The rollout status sums the # other resources, unified by an `app=` label. The rollout status sums the
# Kubernetes deployments together. # Kubernetes deployments together.
class RolloutStatus class RolloutStatus
attr_reader :deployments, :instances, :completion attr_reader :deployments, :instances, :completion, :status
def complete? def complete?
completion == 100 completion == 100
end end
def valid? def loading?
@valid @status == :loading
end
def not_found?
@status == :not_found
end
def found?
@status == :found
end end
def self.from_specs(*specs) def self.from_specs(*specs)
return new([], valid: false) if specs.empty? return new([], status: :not_found) if specs.empty?
deployments = specs.map { |spec| ::Gitlab::Kubernetes::Deployment.new(spec) } deployments = specs.map { |spec| ::Gitlab::Kubernetes::Deployment.new(spec) }
deployments.sort_by!(&:order) deployments.sort_by!(&:order)
new(deployments) new(deployments)
end end
def initialize(deployments, valid: true) def self.loading
@valid = valid new([], status: :loading)
end
def initialize(deployments, status: :found)
@status = status
@deployments = deployments @deployments = deployments
@instances = deployments.flat_map(&:instances) @instances = deployments.flat_map(&:instances)
......
require 'spec_helper' require 'spec_helper'
describe Projects::EnvironmentsController do describe Projects::EnvironmentsController do
include KubernetesHelpers
set(:user) { create(:user) } set(:user) { create(:user) }
set(:project) { create(:project) } set(:project) { create(:project) }
...@@ -21,11 +23,19 @@ describe Projects::EnvironmentsController do ...@@ -21,11 +23,19 @@ describe Projects::EnvironmentsController do
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
end end
it 'expires etag cache to force reload environments list' do
expect_any_instance_of(Gitlab::EtagCaching::Store)
.to receive(:touch).with(project_environments_path(project, format: :json))
get :index, environment_params
end
end end
context 'when requesting JSON response for folders' do context 'when requesting JSON response for folders' do
before do before do
allow_any_instance_of(Environment).to receive(:deployment_service_ready?).and_return(true) allow_any_instance_of(Environment).to receive(:deployment_service_ready?).and_return(true)
allow_any_instance_of(Environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
create(:environment, project: project, create(:environment, project: project,
name: 'staging/review-1', name: 'staging/review-1',
...@@ -49,14 +59,18 @@ describe Projects::EnvironmentsController do ...@@ -49,14 +59,18 @@ describe Projects::EnvironmentsController do
get :index, environment_params(format: :json, scope: :available) get :index, environment_params(format: :json, scope: :available)
end end
it 'responds with matching schema' do
expect(response).to match_response_schema('environments')
end
it 'responds with a payload describing available environments' do it 'responds with a payload describing available environments' do
expect(environments.count).to eq 2 expect(environments.count).to eq 2
expect(environments.first['name']).to eq 'production' expect(environments.first['name']).to eq 'production'
expect(environments.first['latest']['rollout_status_path']).to be_present expect(environments.first['latest']['rollout_status']).to be_present
expect(environments.second['name']).to eq 'staging' expect(environments.second['name']).to eq 'staging'
expect(environments.second['size']).to eq 2 expect(environments.second['size']).to eq 2
expect(environments.second['latest']['name']).to eq 'staging/review-2' expect(environments.second['latest']['name']).to eq 'staging/review-2'
expect(environments.second['latest']['rollout_status_path']).to be_present expect(environments.second['latest']['rollout_status']).to be_present
end end
it 'contains values describing environment scopes sizes' do it 'contains values describing environment scopes sizes' do
...@@ -96,8 +110,8 @@ describe Projects::EnvironmentsController do ...@@ -96,8 +110,8 @@ describe Projects::EnvironmentsController do
end end
it 'does not return the rollout_status_path attribute' do it 'does not return the rollout_status_path attribute' do
expect(environments.first['latest']['rollout_status_path']).to be_blank expect(environments.first['latest']['rollout_status']).not_to be_present
expect(environments.second['latest']['rollout_status_path']).to be_blank expect(environments.second['latest']['rollout_status']).not_to be_present
end end
end end
end end
...@@ -289,59 +303,6 @@ describe Projects::EnvironmentsController do ...@@ -289,59 +303,6 @@ describe Projects::EnvironmentsController do
end end
end end
describe 'GET #status' do
context 'without deployment service' do
it 'returns 404' do
get :status, environment_params
expect(response.status).to eq(404)
end
end
context 'with deployment service' do
let(:project) { create(:kubernetes_project) }
let(:environment) { create(:environment, name: 'production', project: project) }
before do
stub_licensed_features(deploy_board: true)
allow_any_instance_of(Environment).to receive(:deployment_service_ready?).and_return(true)
end
it 'returns 204 until the rollout status is present' do
expect_any_instance_of(Environment)
.to receive(:rollout_status)
.and_return(nil)
get :status, environment_params
expect(response.status).to eq(204)
expect(response.headers['Poll-Interval']).to eq("3000")
end
it 'returns the rollout status when present' do
expect_any_instance_of(Environment)
.to receive(:rollout_status)
.and_return(::Gitlab::Kubernetes::RolloutStatus.new([]))
get :status, environment_params
expect(response.status).to eq(200)
end
end
context 'when license does not has the GitLab_DeployBoard add-on' do
before do
stub_licensed_features(deploy_board: false)
end
it 'does not return any data' do
get :status, environment_params
expect(response).to have_http_status(:not_found)
end
end
end
describe 'GET #metrics' do describe 'GET #metrics' do
before do before do
allow(controller).to receive(:environment).and_return(environment) allow(controller).to receive(:environment).and_return(environment)
......
...@@ -9,6 +9,7 @@ describe KubernetesService, models: true, use_clean_rails_memory_store_caching: ...@@ -9,6 +9,7 @@ describe KubernetesService, models: true, use_clean_rails_memory_store_caching:
describe '#rollout_status' do describe '#rollout_status' do
let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
subject(:rollout_status) { service.rollout_status(environment) } subject(:rollout_status) { service.rollout_status(environment) }
context 'with valid deployments' do context 'with valid deployments' do
...@@ -24,5 +25,30 @@ describe KubernetesService, models: true, use_clean_rails_memory_store_caching: ...@@ -24,5 +25,30 @@ describe KubernetesService, models: true, use_clean_rails_memory_store_caching:
expect(rollout_status.deployments.map(&:labels)).to eq([{ 'app' => 'env-000000' }]) expect(rollout_status.deployments.map(&:labels)).to eq([{ 'app' => 'env-000000' }])
end end
end end
context 'with empty list of deployments' do
before do
stub_reactive_cache(
service,
deployments: []
)
end
it 'creates a matching RolloutStatus' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status).to be_not_found
end
end
context 'not yet loaded deployments' do
before do
stub_reactive_cache
end
it 'creates a matching RolloutStatus' do
expect(rollout_status).to be_kind_of(::Gitlab::Kubernetes::RolloutStatus)
expect(rollout_status).to be_loading
end
end
end end
end end
{
"additionalProperties": false,
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"iid": {
"type": "integer"
},
"last?": {
"type": "boolean"
},
"ref": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
},
"sha": {
"type": "string"
},
"tag": {
"type": "boolean"
}
},
"required": [
"sha",
"created_at",
"iid",
"tag",
"last?",
"ref",
"id"
],
"type": "object"
}
...@@ -3,49 +3,7 @@ ...@@ -3,49 +3,7 @@
"properties": { "properties": {
"deployments": { "deployments": {
"items": { "items": {
"additionalProperties": false, "$ref": "deployment.json"
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"iid": {
"type": "integer"
},
"last?": {
"type": "boolean"
},
"ref": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
},
"sha": {
"type": "string"
},
"tag": {
"type": "boolean"
}
},
"required": [
"sha",
"created_at",
"iid",
"tag",
"last?",
"ref",
"id"
],
"type": "object"
}, },
"minItems": 1, "minItems": 1,
"type": "array" "type": "array"
......
{
"type": "object",
"additionalProperties": false,
"required": [
"id",
"name",
"state",
"last_deployment",
"environment_path",
"created_at",
"updated_at"
],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"state": {
"type": "string"
},
"external_url": {
"type": "string"
},
"environment_type": {
"type": [
"string",
"null"
]
},
"last_deployment": {
"oneOf": [
{
"$ref": "deployment.json"
},
{
"type": ["null"]
}
]
},
"stop_action?": {
"type": "boolean"
},
"rollout_status": {
"$ref": "rollout_status.json"
},
"environment_path": {
"type": "string"
},
"stop_path": {
"type": "string"
},
"terminal_path": {
"type": "string"
},
"folder_path": {
"type": "string"
},
"created_at": {
"type": "date"
},
"updated_at": {
"type": "date"
}
}
}
{
"additionalProperties": false,
"properties": {
"environments": {
"items": {
"$ref": "environments_group.json"
},
"minItems": 1,
"type": "array"
},
"available_count": {
"type": "integer"
},
"stopped_count": {
"type": "integer"
}
},
"required": [
"environments",
"available_count",
"stopped_count"
],
"type": "object"
}
{
"type": "object",
"required": [
"name",
"size",
"latest"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"size": {
"type": "integer"
},
"latest": {
"$ref": "environment.json"
}
}
}
{
"type": "object",
"additionalProperties": false,
"required": [
"status"
],
"properties": {
"status": {
"type": "string"
},
"completion": {
"type": "integer"
},
"is_completed": {
"type": "boolean"
},
"instances": {
"type": "array",
"items": {
"additionalProperties": false,
"type": "object",
"required": [
"status",
"tooltip",
"track",
"stable"
],
"properties": {
"status": {
"type": "string"
},
"tooltip": {
"type": "string"
},
"track": {
"type": "string"
},
"stable": {
"type": "boolean"
}
}
}
}
}
}
import Vue from 'vue'; import Vue from 'vue';
import DeployBoard from '~/environments/components/deploy_board_component.vue'; import DeployBoard from '~/environments/components/deploy_board_component.vue';
import { deployBoardMockData, invalidDeployBoardMockData } from './mock_data'; import { deployBoardMockData } from './mock_data';
describe('Deploy Board', () => { describe('Deploy Board', () => {
let DeployBoardComponent; let DeployBoardComponent;
...@@ -17,7 +17,7 @@ describe('Deploy Board', () => { ...@@ -17,7 +17,7 @@ describe('Deploy Board', () => {
propsData: { propsData: {
deployBoardData: deployBoardMockData, deployBoardData: deployBoardMockData,
isLoading: false, isLoading: false,
hasError: false, isEmpty: false,
}, },
}).$mount(); }).$mount();
}); });
...@@ -46,15 +46,15 @@ describe('Deploy Board', () => { ...@@ -46,15 +46,15 @@ describe('Deploy Board', () => {
}); });
}); });
describe('without valid data', () => { describe('with empty state', () => {
let component; let component;
beforeEach(() => { beforeEach(() => {
component = new DeployBoardComponent({ component = new DeployBoardComponent({
propsData: { propsData: {
deployBoardData: invalidDeployBoardMockData, deployBoardData: {},
isLoading: false, isLoading: false,
hasError: false, isEmpty: true,
}, },
}).$mount(); }).$mount();
}); });
...@@ -65,21 +65,21 @@ describe('Deploy Board', () => { ...@@ -65,21 +65,21 @@ describe('Deploy Board', () => {
}); });
}); });
describe('with error', () => { describe('with loading state', () => {
let component; let component;
beforeEach(() => { beforeEach(() => {
component = new DeployBoardComponent({ component = new DeployBoardComponent({
propsData: { propsData: {
deployBoardData: {}, deployBoardData: {},
isLoading: false, isLoading: true,
hasError: true, isEmpty: false,
}, },
}).$mount(); }).$mount();
}); });
it('should render empty state', () => { it('should render loading spinner', () => {
expect(component.$el.children.length).toEqual(1); expect(component.$el.querySelector('.fa-spin')).toBeDefined();
}); });
}); });
}); });
...@@ -37,12 +37,11 @@ describe('Environment item', () => { ...@@ -37,12 +37,11 @@ describe('Environment item', () => {
size: 1, size: 1,
environment_path: 'url', environment_path: 'url',
id: 1, id: 1,
rollout_status_path: 'url',
hasDeployBoard: true, hasDeployBoard: true,
deployBoardData: deployBoardMockData, deployBoardData: deployBoardMockData,
isDeployBoardVisible: true, isDeployBoardVisible: true,
isLoadingDeployBoard: false, isLoadingDeployBoard: false,
hasErrorDeployBoard: false, isEmptyDeployBoard: false,
}; };
const component = new EnvironmentTable({ const component = new EnvironmentTable({
...@@ -66,7 +65,6 @@ describe('Environment item', () => { ...@@ -66,7 +65,6 @@ describe('Environment item', () => {
size: 1, size: 1,
environment_path: 'url', environment_path: 'url',
id: 1, id: 1,
rollout_status_path: 'url',
hasDeployBoard: true, hasDeployBoard: true,
deployBoardData: { deployBoardData: {
instances: [ instances: [
......
...@@ -29,12 +29,12 @@ describe('Store', () => { ...@@ -29,12 +29,12 @@ describe('Store', () => {
stop_path: '/root/review-app/environments/7/stop', stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z', created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z', updated_at: '2017-01-31T10:53:46.894Z',
rollout_status_path: '/path', rollout_status: {},
hasDeployBoard: true, hasDeployBoard: true,
isDeployBoardVisible: false, isDeployBoardVisible: true,
deployBoardData: {}, deployBoardData: {},
isLoadingDeployBoard: false, isLoadingDeployBoard: false,
hasErrorDeployBoard: false, isEmptyDeployBoard: false,
}; };
store.storeEnvironments(serverData); store.storeEnvironments(serverData);
...@@ -58,20 +58,20 @@ describe('Store', () => { ...@@ -58,20 +58,20 @@ describe('Store', () => {
expect(store.state.environments.length).toEqual(serverData.length); expect(store.state.environments.length).toEqual(serverData.length);
}); });
it('should store a non folder environment with deploy board if rollout_status_path key is provided', () => { it('should store a non folder environment with deploy board if rollout_status key is provided', () => {
const environment = { const environment = {
name: 'foo', name: 'foo',
size: 1, size: 1,
latest: { latest: {
id: 1, id: 1,
rollout_status_path: 'url', rollout_status: deployBoardMockData,
}, },
}; };
store.storeEnvironments([environment]); store.storeEnvironments([environment]);
expect(store.state.environments[0].hasDeployBoard).toEqual(true); expect(store.state.environments[0].hasDeployBoard).toEqual(true);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(false); expect(store.state.environments[0].isDeployBoardVisible).toEqual(true);
expect(store.state.environments[0].deployBoardData).toEqual({}); expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
}); });
it('should add folder keys when environment is a folder', () => { it('should add folder keys when environment is a folder', () => {
...@@ -192,7 +192,7 @@ describe('Store', () => { ...@@ -192,7 +192,7 @@ describe('Store', () => {
latest: { latest: {
id: 1, id: 1,
}, },
rollout_status_path: 'path', rollout_status: deployBoardMockData,
}; };
store.storeEnvironments([environment]); store.storeEnvironments([environment]);
...@@ -201,16 +201,10 @@ describe('Store', () => { ...@@ -201,16 +201,10 @@ describe('Store', () => {
it('should toggle deploy board property for given environment id', () => { it('should toggle deploy board property for given environment id', () => {
store.toggleDeployBoard(1); store.toggleDeployBoard(1);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(true); expect(store.state.environments[0].isDeployBoardVisible).toEqual(false);
});
it('should store deploy board data for given environment id', () => {
store.storeDeployBoard(1, deployBoardMockData);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
}); });
it('should keep deploy board data when updating environments', () => { it('should keep deploy board data when updating environments', () => {
store.storeDeployBoard(1, deployBoardMockData);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData); expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
const environment = { const environment = {
...@@ -219,7 +213,7 @@ describe('Store', () => { ...@@ -219,7 +213,7 @@ describe('Store', () => {
latest: { latest: {
id: 1, id: 1,
}, },
rollout_status_path: 'path', rollout_status: deployBoardMockData,
}; };
store.storeEnvironments([environment]); store.storeEnvironments([environment]);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData); expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
...@@ -243,12 +237,12 @@ describe('Store', () => { ...@@ -243,12 +237,12 @@ describe('Store', () => {
latest: { latest: {
id: 1, id: 1,
}, },
rollout_status_path: 'path', rollout_status: deployBoardMockData,
}; };
store.storeEnvironments([environment]); store.storeEnvironments([environment]);
expect(store.getOpenDeployBoards().length).toEqual(0); expect(store.getOpenDeployBoards().length).toEqual(1);
}); });
}); });
}); });
...@@ -12,7 +12,7 @@ export const environmentsList = [ ...@@ -12,7 +12,7 @@ export const environmentsList = [
stop_path: '/root/review-app/environments/7/stop', stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z', created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z', updated_at: '2017-01-31T10:53:46.894Z',
rollout_status_path: '/path', rollout_status: {},
}, },
{ {
folderName: 'build', folderName: 'build',
...@@ -28,7 +28,7 @@ export const environmentsList = [ ...@@ -28,7 +28,7 @@ export const environmentsList = [
stop_path: '/root/review-app/environments/12/stop', stop_path: '/root/review-app/environments/12/stop',
created_at: '2017-02-01T19:42:18.400Z', created_at: '2017-02-01T19:42:18.400Z',
updated_at: '2017-02-01T19:42:18.400Z', updated_at: '2017-02-01T19:42:18.400Z',
rollout_status_path: '/path', rollout_status: {},
}, },
]; ];
...@@ -48,7 +48,7 @@ export const serverData = [ ...@@ -48,7 +48,7 @@ export const serverData = [
stop_path: '/root/review-app/environments/7/stop', stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z', created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z', updated_at: '2017-01-31T10:53:46.894Z',
rollout_status_path: '/path', rollout_status: {},
}, },
}, },
{ {
...@@ -100,7 +100,7 @@ export const environment = { ...@@ -100,7 +100,7 @@ export const environment = {
stop_path: '/root/review-app/environments/7/stop', stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z', created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z', updated_at: '2017-01-31T10:53:46.894Z',
rollout_status_path: '/path', rollout_status: {},
}; };
export const deployBoardMockData = { export const deployBoardMockData = {
...@@ -136,15 +136,7 @@ export const deployBoardMockData = { ...@@ -136,15 +136,7 @@ export const deployBoardMockData = {
abort_url: 'url', abort_url: 'url',
rollback_url: 'url', rollback_url: 'url',
completion: 100, completion: 100,
valid: true, status: 'found',
};
export const invalidDeployBoardMockData = {
instances: [],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
valid: false,
}; };
export const folder = { export const folder = {
......
...@@ -94,15 +94,33 @@ describe Gitlab::Kubernetes::RolloutStatus do ...@@ -94,15 +94,33 @@ describe Gitlab::Kubernetes::RolloutStatus do
end end
end end
describe '#valid?' do describe '#not_found?' do
context 'when the specs are passed' do context 'when the specs are passed' do
it { is_expected.to be_valid } it { is_expected.not_to be_not_found }
end end
context 'when no specs are passed' do context 'when list of specs is empty' do
let(:specs) { specs_none } let(:specs) { specs_none }
it { is_expected.not_to be_valid } it { is_expected.to be_not_found }
end end
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) { specs_none }
it { is_expected.not_to be_found }
end
end
describe '.loading' do
subject { described_class.loading }
it { is_expected.to be_loading }
end
end end
...@@ -115,6 +115,13 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do ...@@ -115,6 +115,13 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
go! go!
end end
it "calls a reactive_cache_updated only once if content did not change on subsequent update" do
expect(instance).to receive(:calculate_reactive_cache).twice
expect(instance).to receive(:reactive_cache_updated).once
2.times { instance.exclusively_update_reactive_cache! }
end
context 'and #calculate_reactive_cache raises an exception' do context 'and #calculate_reactive_cache raises an exception' do
before do before do
stub_reactive_cache(instance, "preexisting") stub_reactive_cache(instance, "preexisting")
......
...@@ -385,6 +385,7 @@ describe Environment do ...@@ -385,6 +385,7 @@ describe Environment do
describe '#rollout_status' do describe '#rollout_status' do
let(:project) { create(:kubernetes_project) } let(:project) { create(:kubernetes_project) }
subject { environment.rollout_status } subject { environment.rollout_status }
context 'when the environment has rollout status' do context 'when the environment has rollout status' do
......
require 'spec_helper' require 'spec_helper'
describe EnvironmentEntity do describe EnvironmentEntity do
include KubernetesHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:environment) { create(:environment) } let(:environment) { create(:environment) }
...@@ -50,12 +52,11 @@ describe EnvironmentEntity do ...@@ -50,12 +52,11 @@ describe EnvironmentEntity do
before do before do
stub_licensed_features(deploy_board: true) stub_licensed_features(deploy_board: true)
allow(environment).to receive(:deployment_service_ready?).and_return(true) allow(environment).to receive(:deployment_service_ready?).and_return(true)
allow(environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
end end
it 'exposes rollout_status_path' do it 'exposes rollout_status' do
expected = '/' + [environment.project.full_path, 'environments', environment.id, 'status.json'].join('/') expect(subject).to include(:rollout_status)
expect(subject[:rollout_status_path]).to eq(expected)
end end
end end
...@@ -65,7 +66,7 @@ describe EnvironmentEntity do ...@@ -65,7 +66,7 @@ describe EnvironmentEntity do
allow(environment).to receive(:deployment_service_ready?).and_return(true) allow(environment).to receive(:deployment_service_ready?).and_return(true)
end end
it 'does not expose rollout_status_path' do it 'does not expose rollout_status' do
expect(subject[:rollout_status_path]).to be_blank expect(subject[:rollout_status_path]).to be_blank
end end
end end
......
...@@ -54,7 +54,7 @@ describe EnvironmentSerializer do ...@@ -54,7 +54,7 @@ describe EnvironmentSerializer do
context 'when representing environments within folders' do context 'when representing environments within folders' do
let(:serializer) do let(:serializer) do
described_class.new(project: project).within_folders described_class.new(current_user: user, project: project).within_folders
end end
let(:resource) { Environment.all } let(:resource) { Environment.all }
...@@ -123,7 +123,7 @@ describe EnvironmentSerializer do ...@@ -123,7 +123,7 @@ describe EnvironmentSerializer do
let(:pagination) { { page: 1, per_page: 2 } } let(:pagination) { { page: 1, per_page: 2 } }
let(:serializer) do let(:serializer) do
described_class.new(project: project) described_class.new(current_user: user, project: project)
.with_pagination(request, response) .with_pagination(request, response)
end end
...@@ -169,7 +169,7 @@ describe EnvironmentSerializer do ...@@ -169,7 +169,7 @@ describe EnvironmentSerializer do
context 'when grouping environments within folders' do context 'when grouping environments within folders' do
let(:serializer) do let(:serializer) do
described_class.new(project: project) described_class.new(current_user: user, project: project)
.with_pagination(request, response) .with_pagination(request, response)
.within_folders .within_folders
end end
......
...@@ -7,11 +7,29 @@ describe RolloutStatusEntity do ...@@ -7,11 +7,29 @@ describe RolloutStatusEntity do
described_class.new(rollout_status, request: double) described_class.new(rollout_status, request: double)
end end
let(:rollout_status) { ::Gitlab::Kubernetes::RolloutStatus.from_specs(kube_deployment) }
subject { entity.as_json } subject { entity.as_json }
it { is_expected.to have_key(:instances) } context 'when kube deployment is valid' do
it { is_expected.to have_key(:completion) } let(:rollout_status) { kube_deployment_rollout_status }
it { is_expected.to have_key(:is_completed) }
it { is_expected.to have_key(:valid) } it "exposes status" do
is_expected.to include(:status)
end
it "exposes deployment data" do
is_expected.to include(:instances, :completion, :is_completed)
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)
end
end
end end
...@@ -122,4 +122,12 @@ module KubernetesHelpers ...@@ -122,4 +122,12 @@ module KubernetesHelpers
terminal terminal
end end
end end
def kube_deployment_rollout_status
::Gitlab::Kubernetes::RolloutStatus.from_specs(kube_deployment)
end
def empty_deployment_rollout_status
::Gitlab::Kubernetes::RolloutStatus.from_specs()
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment