Commit 6003e710 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch 'feature/geo-node-status-ajax' into 'master'

Get Geo secondaries nodes statuses over AJAX

See merge request !1438
parents d83414e6 84e5f5a1
......@@ -43,6 +43,8 @@ import GroupsList from './groups_list';
import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import GeoNodes from './geo_nodes';
const ShortcutsBlob = require('./shortcuts_blob');
const UserCallout = require('./user_callout');
......@@ -348,6 +350,9 @@ const UserCallout = require('./user_callout');
case 'abuse_reports':
new gl.AbuseReports();
break;
case 'geo_nodes':
new GeoNodes($('.geo-nodes'));
break;
}
break;
case 'dashboard':
......
/* eslint-disable no-new*/
import './smart_interval';
const healthyClass = 'geo-node-icon-healthy';
const unhealthyClass = 'geo-node-icon-unhealthy';
class GeoNodeStatus {
constructor(el) {
this.$el = $(el);
this.$icon = $('.js-geo-node-icon', this.$el);
this.$status = $('.js-geo-node-status', this.$el);
this.$repositoriesSynced = $('.js-repositories-synced', this.$status);
this.$repositoriesFailed = $('.js-repositories-failed', this.$status);
this.$lfsObjectsSynced = $('.js-lfs-objects-synced', this.$status);
this.$health = $('.js-health', this.$status);
this.endpoint = this.$el.data('status-url');
this.statusInterval = new gl.SmartInterval({
callback: this.getStatus.bind(this),
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
immediateExecution: true,
});
}
getStatus() {
$.getJSON(this.endpoint, (status) => {
this.setStatusIcon(status.healthy);
this.$repositoriesSynced.html(`${status.repositories_synced_count}/${status.repositories_count} (${status.repositories_synced_in_percentage})`);
this.$repositoriesFailed.html(status.repositories_failed_count);
this.$lfsObjectsSynced.html(`${status.lfs_objects_synced_count}/${status.lfs_objects_count} (${status.lfs_objects_synced_in_percentage})`);
this.$health.html(status.health);
this.$status.show();
});
}
setStatusIcon(healthy) {
if (healthy) {
this.$icon.removeClass(unhealthyClass)
.addClass(healthyClass)
.attr('title', 'Healthy');
} else {
this.$icon.removeClass(healthyClass)
.addClass(unhealthyClass)
.attr('title', 'Unhealthy');
}
this.$icon.tooltip('fixTitle');
}
}
class GeoNodes {
constructor(container) {
this.$container = $(container);
this.pollForSecondaryNodeStatus();
}
pollForSecondaryNodeStatus() {
$('.js-geo-secondary-node', this.$container).each((i, el) => {
new GeoNodeStatus(el);
});
}
}
export default GeoNodes;
class Admin::GeoNodesController < Admin::ApplicationController
before_action :check_license, except: [:index, :destroy]
before_action :load_node, only: [:destroy, :repair, :toggle]
before_action :load_node, only: [:destroy, :repair, :toggle, :status]
def index
# Ensure all nodes are using their Presenter
@nodes = GeoNode.all.map(&:present)
@nodes = GeoNode.all
@node = GeoNode.new
unless Gitlab::Geo.license_allows?
......@@ -57,6 +56,16 @@ class Admin::GeoNodesController < Admin::ApplicationController
redirect_to admin_geo_nodes_path
end
def status
status = Geo::NodeStatusService.new.call(@node)
respond_to do |format|
format.json do
render json: GeoNodeStatusSerializer.new.represent(status)
end
end
end
private
def geo_node_params
......
......@@ -4,15 +4,10 @@ module EE
if node.primary?
icon 'star fw', class: 'has-tooltip', title: 'Primary node'
else
status =
if node.enabled?
node.healthy? ? 'healthy' : 'unhealthy'
else
'disabled'
end
status = node.enabled? ? 'healthy' : 'disabled'
icon 'globe fw',
class: "geo-node-icon-#{status} has-tooltip",
class: "js-geo-node-icon geo-node-icon-#{status} has-tooltip",
title: status.capitalize
end
end
......
class GeoNodeStatus
include ActiveModel::Model
attr_accessor :id
attr_writer :health
def health
......@@ -39,24 +40,24 @@ class GeoNodeStatus
@repositories_failed_count = value.to_i
end
def lfs_objects_total
@lfs_objects_total ||= LfsObject.count
def lfs_objects_count
@lfs_objects_count ||= LfsObject.count
end
def lfs_objects_total=(value)
@lfs_objects_total = value.to_i
def lfs_objects_count=(value)
@lfs_objects_count = value.to_i
end
def lfs_objects_synced
@lfs_objects_synced ||= Geo::FileRegistry.where(file_type: :lfs).count
def lfs_objects_synced_count
@lfs_objects_synced_count ||= Geo::FileRegistry.where(file_type: :lfs).count
end
def lfs_objects_synced=(value)
@lfs_objects_synced = value.to_i
def lfs_objects_synced_count=(value)
@lfs_objects_synced_count = value.to_i
end
def lfs_objects_synced_in_percentage
sync_percentage(lfs_objects_total, lfs_objects_synced)
sync_percentage(lfs_objects_count, lfs_objects_synced_count)
end
private
......
class GeoNodePresenter < Gitlab::View::Presenter::Delegated
presents :geo_node
delegate :healthy?, :health, :repositories_count, :repositories_synced_count,
:repositories_synced_in_percentage, :repositories_failed_count,
:lfs_objects_total, :lfs_objects_synced, :lfs_objects_synced_in_percentage,
to: :status
private
def status
@status ||= Geo::NodeStatusService.new.call(geo_node.status_url)
end
end
class GeoNodeStatusEntity < Grape::Entity
include ActionView::Helpers::NumberHelper
expose :id
expose :healthy?, as: :healthy
expose :health do |node|
node.healthy? ? 'No Health Problems Detected' : node.health
end
expose :lfs_objects_count
expose :lfs_objects_synced_count
expose :lfs_objects_synced_in_percentage do |node|
number_to_percentage(node.lfs_objects_synced_in_percentage, precision: 2)
end
expose :repositories_count
expose :repositories_failed_count
expose :repositories_synced_count
expose :repositories_synced_in_percentage do |node|
number_to_percentage(node.repositories_synced_in_percentage, precision: 2)
end
end
class GeoNodeStatusSerializer < BaseSerializer
entity GeoNodeStatusEntity
end
......@@ -3,12 +3,19 @@ module Geo
include Gitlab::CurrentSettings
include HTTParty
KEYS = %w(health repositories_count repositories_synced_count repositories_failed_count lfs_objects_total lfs_objects_synced).freeze
KEYS = %w(
health
repositories_count
repositories_synced_count
repositories_failed_count
lfs_objects_count
lfs_objects_synced_count
).freeze
def call(status_url)
def call(geo_node)
values =
begin
response = self.class.get(status_url, headers: headers, timeout: timeout)
response = self.class.get(geo_node.status_url, headers: headers, timeout: timeout)
if response.success?
response.parsed_response.values_at(*KEYS)
......@@ -19,7 +26,7 @@ module Geo
[e.message]
end
GeoNodeStatus.new(KEYS.zip(values).to_h)
GeoNodeStatus.new(KEYS.zip(values).to_h.merge(id: geo_node.id))
end
private
......
......@@ -14,9 +14,9 @@
.panel.panel-default
.panel-heading
Geo nodes (#{@nodes.count})
%ul.well-list
%ul.well-list.geo-nodes
- @nodes.each do |node|
%li
%li{ id: dom_id(node), class: ('js-geo-secondary-node' if node.secondary?), data: { status_url: status_admin_geo_node_path(node) } }
.list-item-name
%span
= node_status_icon(node)
......@@ -24,17 +24,21 @@
- if node.primary?
%span.help-block Primary node
- else
%p
%span.help-block
Repositories synced: #{node.repositories_synced_count}/#{node.repositories_count} (#{number_to_percentage(node.repositories_synced_in_percentage, precision: 2)})
%p
%span.help-block
Repositories failed: #{node.repositories_failed_count}
%p
%span.help-block
LFS objects synced: #{node.lfs_objects_synced}/#{node.lfs_objects_total} (#{number_to_percentage(node.lfs_objects_synced_in_percentage, precision: 2)})
%p
%span.help-block= node.healthy? ? 'No Health Problems Detected' : node.health
.js-geo-node-status{ style: 'display: none' }
%p
%span.help-block
Repositories synced:
%span.js-repositories-synced
%p
%span.help-block
Repositories failed:
%span.js-repositories-failed
%p
%span.help-block
LFS objects synced:
%span.js-lfs-objects-synced
%p
%span.help-block.js-health
.pull-right
- if Gitlab::Geo.license_allows?
......
---
title: Get Geo secondaries nodes statuses over AJAX
merge_request:
author:
......@@ -119,6 +119,7 @@ namespace :admin do
member do
post :repair
post :toggle
get :status
end
end
## EE-specific
......
......@@ -782,8 +782,8 @@ module API
expose :repositories_count
expose :repositories_synced_count
expose :repositories_failed_count
expose :lfs_objects_total
expose :lfs_objects_synced
expose :lfs_objects_count
expose :lfs_objects_synced_count
end
class PersonalAccessToken < Grape::Entity
......
......@@ -195,4 +195,41 @@ describe Admin::GeoNodesController do
end
end
end
describe '#status' do
let(:geo_node) { create(:geo_node) }
context 'without add-on license' do
before do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(false)
get :status, id: geo_node, format: :json
end
it_behaves_like 'unlicensed geo action'
end
context 'with add-on license' do
let(:geo_node_status) do
GeoNodeStatus.new(
id: 1,
health: nil,
lfs_objects_count: 256,
lfs_objects_synced_count: 123,
repositories_count: 10,
repositories_synced_count: 5
)
end
before do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(true)
allow_any_instance_of(Geo::NodeStatusService).to receive(:call).and_return(geo_node_status)
end
it 'returns the status' do
get :status, id: geo_node, format: :json
expect(response).to match_response_schema('geo_node_status')
end
end
end
end
{
"type": "object",
"required" : [
"id",
"healthy",
"health",
"lfs_objects_count",
"lfs_objects_synced_count",
"lfs_objects_synced_in_percentage",
"repositories_count",
"repositories_failed_count",
"repositories_synced_count",
"repositories_synced_in_percentage"
],
"properties" : {
"id": { "type": "integer" },
"healthy": { "type": "boolean" },
"health": { "type": "string" },
"lfs_objects_count": { "type": "integer" },
"lfs_objects_synced_count": { "type": "integer" },
"lfs_objects_synced_in_percentage": { "type": "string" },
"repositories_count": { "type": "integer" },
"repositories_failed_count": { "type": "integer" },
"repositories_synced_count": { "type": "integer" },
"repositories_synced_in_percentage": { "type": "string" }
},
"additionalProperties": false
}
require 'spec_helper'
describe Geo::GeoNodeStatus, models: true do
subject { GeoNodeStatus.new }
describe '#lfs_objects_synced_in_percentage' do
it 'returns 0 when no objects are available' do
subject.lfs_objects_total = 0
subject.lfs_objects_synced = 0
expect(subject.lfs_objects_synced_in_percentage).to eq(0)
end
it 'returns the right percentage' do
subject.lfs_objects_total = 4
subject.lfs_objects_synced = 1
expect(subject.lfs_objects_synced_in_percentage).to be_within(0.0001).of(25)
end
end
end
require 'spec_helper'
describe GeoNodeStatus, model: true do
subject { described_class.new }
describe '#healthy?' do
context 'when health is blank' do
it 'returns true' do
subject.health = ''
expect(subject.healthy?).to eq true
end
end
context 'when health is present' do
it 'returns false' do
subject.health = 'something went wrong'
expect(subject.healthy?).to eq false
end
end
end
describe '#health' do
it 'delegates to the HealthCheck' do
subject.health = nil
expect(HealthCheck::Utils).to receive(:process_checks).with(['geo']).once
subject.health
end
end
describe '#lfs_objects_synced_in_percentage' do
it 'returns 0 when no objects are available' do
subject.lfs_objects_count = 0
subject.lfs_objects_synced_count = 0
expect(subject.lfs_objects_synced_in_percentage).to eq(0)
end
it 'returns the right percentage' do
subject.lfs_objects_count = 4
subject.lfs_objects_synced_count = 1
expect(subject.lfs_objects_synced_in_percentage).to be_within(0.0001).of(25)
end
end
describe '#repositories_synced_in_percentage' do
it 'returns 0 when no objects are available' do
subject.repositories_count = 0
subject.repositories_synced_count = 0
expect(subject.repositories_synced_in_percentage).to eq(0)
end
it 'returns the right percentage' do
subject.repositories_count = 4
subject.repositories_synced_count = 1
expect(subject.repositories_synced_in_percentage).to be_within(0.0001).of(25)
end
end
context 'when no values are available' do
it 'returns 0 for each attribute' do
subject.lfs_objects_count = nil
subject.lfs_objects_synced_count = nil
subject.repositories_count = nil
subject.repositories_synced_count = nil
subject.repositories_failed_count = nil
expect(subject.repositories_count).to be_zero
expect(subject.repositories_synced_count).to be_zero
expect(subject.repositories_synced_in_percentage).to be_zero
expect(subject.repositories_failed_count).to be_zero
expect(subject.lfs_objects_count).to be_zero
expect(subject.lfs_objects_synced_count).to be_zero
expect(subject.lfs_objects_synced_in_percentage).to be_zero
end
end
end
require 'spec_helper'
describe GeoNodeStatusEntity do
let(:geo_node_status) do
GeoNodeStatus.new(
id: 1,
health: nil,
lfs_objects_count: 256,
lfs_objects_synced_count: 123,
repositories_count: 10,
repositories_synced_count: 5
)
end
let(:entity) do
described_class.new(geo_node_status, request: double)
end
let(:error) do
'Could not connect to Geo database'
end
subject { entity.as_json }
it { is_expected.to have_key(:id) }
it { is_expected.to have_key(:healthy) }
it { is_expected.to have_key(:health) }
it { is_expected.to have_key(:lfs_objects_count) }
it { is_expected.to have_key(:lfs_objects_synced_count) }
it { is_expected.to have_key(:lfs_objects_synced_in_percentage) }
it { is_expected.to have_key(:repositories_count) }
it { is_expected.to have_key(:repositories_failed_count) }
it { is_expected.to have_key(:repositories_synced_count)}
it { is_expected.to have_key(:repositories_synced_in_percentage) }
describe '#healthy' do
context 'when node is healthy' do
it 'returns true' do
expect(subject[:healthy]).to eq true
end
end
context 'when node is unhealthy' do
before do
geo_node_status.health = error
end
subject { entity.as_json }
it 'returns false' do
expect(subject[:healthy]).to eq false
end
end
end
describe '#health' do
context 'when node is healthy' do
it 'exposes the health message' do
expect(subject[:health]).to eq 'No Health Problems Detected'
end
end
context 'when node is unhealthy' do
before do
geo_node_status.health = error
end
subject { entity.as_json }
it 'exposes the error message' do
expect(subject[:health]).to eq error
end
end
end
describe '#lfs_objects_synced_in_percentage' do
it 'formats as percentage' do
expect(subject[:lfs_objects_synced_in_percentage]).to eq '48.05%'
end
end
describe '#repositories_synced_in_percentage' do
it 'formats as percentage' do
expect(subject[:repositories_synced_in_percentage]).to eq '50.00%'
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