Commit 8965f0e7 authored by Emily Ring's avatar Emily Ring Committed by Joao Cunha

Show cluster nodes CPU and Memory

Also:
- Adds Sentry tracking and flash error message
- Adds specs
- Updates localization
parent dde42885
<script> <script>
import * as Sentry from '@sentry/browser';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { import {
GlDeprecatedBadge as GlBadge, GlDeprecatedBadge as GlBadge,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlPagination, GlPagination,
GlSprintf,
GlTable, GlTable,
} from '@gitlab/ui'; } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
...@@ -12,11 +14,14 @@ import { CLUSTER_TYPES, STATUSES } from '../constants'; ...@@ -12,11 +14,14 @@ import { CLUSTER_TYPES, STATUSES } from '../constants';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
export default { export default {
nodeMemoryText: __('%{totalMemory} (%{freeSpacePercentage}%{percentSymbol} free)'),
nodeCpuText: __('%{totalCpu} (%{freeSpacePercentage}%{percentSymbol} free)'),
components: { components: {
GlBadge, GlBadge,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlPagination, GlPagination,
GlSprintf,
GlTable, GlTable,
}, },
directives: { directives: {
...@@ -47,15 +52,14 @@ export default { ...@@ -47,15 +52,14 @@ export default {
key: 'node_size', key: 'node_size',
label: __('Nodes'), label: __('Nodes'),
}, },
// Fields are missing calculation methods and not ready to display {
// { key: 'total_cpu',
// key: 'node_cpu', label: __('Total cores (CPUs)'),
// label: __('Total cores (vCPUs)'), },
// }, {
// { key: 'total_memory',
// key: 'node_memory', label: __('Total memory (GB)'),
// label: __('Total memory (GB)'), },
// },
{ {
key: 'cluster_type', key: 'cluster_type',
label: __('Cluster level'), label: __('Cluster level'),
...@@ -72,11 +76,102 @@ export default { ...@@ -72,11 +76,102 @@ export default {
}, },
methods: { methods: {
...mapActions(['fetchClusters', 'setPage']), ...mapActions(['fetchClusters', 'setPage']),
k8sQuantityToGb(quantity) {
if (!quantity) {
return 0;
} else if (quantity.endsWith(__('Ki'))) {
return parseInt(quantity.substr(0, quantity.length - 2), 10) * 0.000001024;
} else if (quantity.endsWith(__('Mi'))) {
return parseInt(quantity.substr(0, quantity.length - 2), 10) * 0.001048576;
}
// We are trying to track quantity types coming from Kubernetes.
// Sentry will notify us if we are missing types.
throw new Error(`UnknownK8sMemoryQuantity:${quantity}`);
},
k8sQuantityToCpu(quantity) {
if (!quantity) {
return 0;
} else if (quantity.endsWith('m')) {
return parseInt(quantity.substr(0, quantity.length - 1), 10) / 1000.0;
} else if (quantity.endsWith('n')) {
return parseInt(quantity.substr(0, quantity.length - 1), 10) / 1000000000.0;
}
// We are trying to track quantity types coming from Kubernetes.
// Sentry will notify us if we are missing types.
throw new Error(`UnknownK8sCpuQuantity:${quantity}`);
},
statusTitle(status) { statusTitle(status) {
const iconTitle = STATUSES[status] || STATUSES.default; const iconTitle = STATUSES[status] || STATUSES.default;
return sprintf(__('Status: %{title}'), { title: iconTitle.title }, false); return sprintf(__('Status: %{title}'), { title: iconTitle.title }, false);
}, },
totalMemoryAndUsage(nodes) {
try {
// For EKS node.usage will not be present unless the user manually
// install the metrics server
if (nodes && nodes[0].usage) {
let totalAllocatableMemory = 0;
let totalUsedMemory = 0;
nodes.reduce((total, node) => {
const allocatableMemoryQuantity = node.status.allocatable.memory;
const allocatableMemoryGb = this.k8sQuantityToGb(allocatableMemoryQuantity);
totalAllocatableMemory += allocatableMemoryGb;
const usedMemoryQuantity = node.usage.memory;
const usedMemoryGb = this.k8sQuantityToGb(usedMemoryQuantity);
totalUsedMemory += usedMemoryGb;
return null;
}, 0);
const freeSpacePercentage = (1 - totalUsedMemory / totalAllocatableMemory) * 100;
return {
totalMemory: totalAllocatableMemory.toFixed(2),
freeSpacePercentage: Math.round(freeSpacePercentage),
};
}
} catch (error) {
Sentry.captureException(error);
}
return { totalMemory: null, freeSpacePercentage: null };
},
totalCpuAndUsage(nodes) {
try {
// For EKS node.usage will not be present unless the user manually
// install the metrics server
if (nodes && nodes[0].usage) {
let totalAllocatableCpu = 0;
let totalUsedCpu = 0;
nodes.reduce((total, node) => {
const allocatableCpuQuantity = node.status.allocatable.cpu;
const allocatableCpu = this.k8sQuantityToCpu(allocatableCpuQuantity);
totalAllocatableCpu += allocatableCpu;
const usedCpuQuantity = node.usage.cpu;
const usedCpuGb = this.k8sQuantityToCpu(usedCpuQuantity);
totalUsedCpu += usedCpuGb;
return null;
}, 0);
const freeSpacePercentage = (1 - totalUsedCpu / totalAllocatableCpu) * 100;
return {
totalCpu: totalAllocatableCpu.toFixed(2),
freeSpacePercentage: Math.round(freeSpacePercentage),
};
}
} catch (error) {
Sentry.captureException(error);
}
return { totalCpu: null, freeSpacePercentage: null };
},
}, },
}; };
</script> </script>
...@@ -109,6 +204,34 @@ export default { ...@@ -109,6 +204,34 @@ export default {
}}</small> }}</small>
</template> </template>
<template #cell(total_cpu)="{ item }">
<span v-if="item.nodes">
<gl-sprintf :message="$options.nodeCpuText">
<template #totalCpu>{{ totalCpuAndUsage(item.nodes).totalCpu }}</template>
<template #freeSpacePercentage>{{
totalCpuAndUsage(item.nodes).freeSpacePercentage
}}</template>
<template #percentSymbol
>%</template
>
</gl-sprintf>
</span>
</template>
<template #cell(total_memory)="{ item }">
<span v-if="item.nodes">
<gl-sprintf :message="$options.nodeMemoryText">
<template #totalMemory>{{ totalMemoryAndUsage(item.nodes).totalMemory }}</template>
<template #freeSpacePercentage>{{
totalMemoryAndUsage(item.nodes).freeSpacePercentage
}}</template>
<template #percentSymbol
>%</template
>
</gl-sprintf>
</span>
</template>
<template #cell(cluster_type)="{value}"> <template #cell(cluster_type)="{value}">
<gl-badge variant="light"> <gl-badge variant="light">
{{ value }} {{ value }}
......
...@@ -373,7 +373,10 @@ module Clusters ...@@ -373,7 +373,10 @@ module Clusters
def retrieve_nodes def retrieve_nodes
result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.get_nodes } result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.get_nodes }
cluster_nodes = result[:response].to_a
return unless result[:response]
cluster_nodes = result[:response]
result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.metrics_client.get_nodes } result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.metrics_client.get_nodes }
nodes_metrics = result[:response].to_a nodes_metrics = result[:response].to_a
......
---
title: Adds cluster CPU and Memory to cluster index
merge_request: 32601
author:
type: changed
...@@ -622,6 +622,12 @@ msgstr "" ...@@ -622,6 +622,12 @@ msgstr ""
msgid "%{token}..." msgid "%{token}..."
msgstr "" msgstr ""
msgid "%{totalCpu} (%{freeSpacePercentage}%{percentSymbol} free)"
msgstr ""
msgid "%{totalMemory} (%{freeSpacePercentage}%{percentSymbol} free)"
msgstr ""
msgid "%{totalWeight} total weight" msgid "%{totalWeight} total weight"
msgstr "" msgstr ""
...@@ -12638,6 +12644,9 @@ msgstr "" ...@@ -12638,6 +12644,9 @@ msgstr ""
msgid "Keyboard shortcuts" msgid "Keyboard shortcuts"
msgstr "" msgstr ""
msgid "Ki"
msgstr ""
msgid "Kubernetes" msgid "Kubernetes"
msgstr "" msgstr ""
...@@ -14095,6 +14104,9 @@ msgstr "" ...@@ -14095,6 +14104,9 @@ msgstr ""
msgid "Metrics|e.g. req/sec" msgid "Metrics|e.g. req/sec"
msgstr "" msgstr ""
msgid "Mi"
msgstr ""
msgid "Microsoft Azure" msgid "Microsoft Azure"
msgstr "" msgstr ""
...@@ -23489,9 +23501,15 @@ msgstr "" ...@@ -23489,9 +23501,15 @@ msgstr ""
msgid "Total artifacts size: %{total_size}" msgid "Total artifacts size: %{total_size}"
msgstr "" msgstr ""
msgid "Total cores (CPUs)"
msgstr ""
msgid "Total issues" msgid "Total issues"
msgstr "" msgstr ""
msgid "Total memory (GB)"
msgstr ""
msgid "Total test time for all commits/merges" msgid "Total test time for all commits/merges"
msgstr "" msgstr ""
......
...@@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import { apiData } from '../mock_data'; import { apiData } from '../mock_data';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlLoadingIcon, GlTable, GlPagination } from '@gitlab/ui'; import { GlLoadingIcon, GlTable, GlPagination } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
describe('Clusters', () => { describe('Clusters', () => {
let mock; let mock;
...@@ -36,7 +37,11 @@ describe('Clusters', () => { ...@@ -36,7 +37,11 @@ describe('Clusters', () => {
}; };
}; };
let captureException;
beforeEach(() => { beforeEach(() => {
captureException = jest.spyOn(Sentry, 'captureException');
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mockPollingApi(200, apiData, paginationHeader()); mockPollingApi(200, apiData, paginationHeader());
...@@ -46,6 +51,7 @@ describe('Clusters', () => { ...@@ -46,6 +51,7 @@ describe('Clusters', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mock.restore(); mock.restore();
captureException.mockRestore();
}); });
describe('clusters table', () => { describe('clusters table', () => {
...@@ -106,8 +112,8 @@ describe('Clusters', () => { ...@@ -106,8 +112,8 @@ describe('Clusters', () => {
${'Unknown'} | ${0} ${'Unknown'} | ${0}
${'1'} | ${1} ${'1'} | ${1}
${'2'} | ${2} ${'2'} | ${2}
${'Unknown'} | ${3} ${'1'} | ${3}
${'Unknown'} | ${4} ${'1'} | ${4}
${'Unknown'} | ${5} ${'Unknown'} | ${5}
`('renders node size for each cluster', ({ nodeSize, lineNumber }) => { `('renders node size for each cluster', ({ nodeSize, lineNumber }) => {
const sizes = findTable().findAll('td:nth-child(3)'); const sizes = findTable().findAll('td:nth-child(3)');
...@@ -115,6 +121,58 @@ describe('Clusters', () => { ...@@ -115,6 +121,58 @@ describe('Clusters', () => {
expect(size.text()).toBe(nodeSize); expect(size.text()).toBe(nodeSize);
}); });
describe('nodes with unknown quantity', () => {
it('notifies Sentry about all missing quantity types', () => {
expect(captureException).toHaveBeenCalledTimes(8);
});
it('notifies Sentry about CPU missing quantity types', () => {
const missingCpuTypeError = new Error('UnknownK8sCpuQuantity:1missingCpuUnit');
expect(captureException).toHaveBeenCalledWith(missingCpuTypeError);
});
it('notifies Sentry about Memory missing quantity types', () => {
const missingMemoryTypeError = new Error('UnknownK8sMemoryQuantity:1missingMemoryUnit');
expect(captureException).toHaveBeenCalledWith(missingMemoryTypeError);
});
});
});
describe('cluster CPU', () => {
it.each`
clusterCpu | lineNumber
${''} | ${0}
${'1.93 (87% free)'} | ${1}
${'3.87 (86% free)'} | ${2}
${'(% free)'} | ${3}
${'(% free)'} | ${4}
${''} | ${5}
`('renders total cpu for each cluster', ({ clusterCpu, lineNumber }) => {
const clusterCpus = findTable().findAll('td:nth-child(4)');
const cpuData = clusterCpus.at(lineNumber);
expect(cpuData.text()).toBe(clusterCpu);
});
});
describe('cluster Memory', () => {
it.each`
clusterMemory | lineNumber
${''} | ${0}
${'5.92 (78% free)'} | ${1}
${'12.86 (79% free)'} | ${2}
${'(% free)'} | ${3}
${'(% free)'} | ${4}
${''} | ${5}
`('renders total memory for each cluster', ({ clusterMemory, lineNumber }) => {
const clusterMemories = findTable().findAll('td:nth-child(5)');
const memoryData = clusterMemories.at(lineNumber);
expect(memoryData.text()).toBe(clusterMemory);
});
}); });
describe('pagination', () => { describe('pagination', () => {
......
...@@ -11,7 +11,12 @@ export const clusterList = [ ...@@ -11,7 +11,12 @@ export const clusterList = [
environment_scope: 'development', environment_scope: 'development',
cluster_type: 'project_type', cluster_type: 'project_type',
status: 'unreachable', status: 'unreachable',
nodes: [{ usage: { cpu: '246155922n', memory: '1255212Ki' } }], nodes: [
{
status: { allocatable: { cpu: '1930m', memory: '5777156Ki' } },
usage: { cpu: '246155922n', memory: '1255212Ki' },
},
],
}, },
{ {
name: 'My Cluster 3', name: 'My Cluster 3',
...@@ -19,8 +24,14 @@ export const clusterList = [ ...@@ -19,8 +24,14 @@ export const clusterList = [
cluster_type: 'project_type', cluster_type: 'project_type',
status: 'authentication_failure', status: 'authentication_failure',
nodes: [ nodes: [
{ usage: { cpu: '246155922n', memory: '1255212Ki' } }, {
{ usage: { cpu: '307051934n', memory: '1379136Ki' } }, status: { allocatable: { cpu: '1930m', memory: '5777156Ki' } },
usage: { cpu: '246155922n', memory: '1255212Ki' },
},
{
status: { allocatable: { cpu: '1940m', memory: '6777156Ki' } },
usage: { cpu: '307051934n', memory: '1379136Ki' },
},
], ],
}, },
{ {
...@@ -28,12 +39,23 @@ export const clusterList = [ ...@@ -28,12 +39,23 @@ export const clusterList = [
environment_scope: 'production', environment_scope: 'production',
cluster_type: 'project_type', cluster_type: 'project_type',
status: 'deleting', status: 'deleting',
nodes: [
{
status: { allocatable: { cpu: '1missingCpuUnit', memory: '1missingMemoryUnit' } },
usage: { cpu: '1missingCpuUnit', memory: '1missingMemoryUnit' },
},
],
}, },
{ {
name: 'My Cluster 5', name: 'My Cluster 5',
environment_scope: 'development', environment_scope: 'development',
cluster_type: 'project_type', cluster_type: 'project_type',
status: 'created', status: 'created',
nodes: [
{
status: { allocatable: { cpu: '1missingCpuUnit', memory: '1missingMemoryUnit' } },
},
],
}, },
{ {
name: 'My Cluster 6', name: 'My Cluster 6',
......
...@@ -1057,7 +1057,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do ...@@ -1057,7 +1057,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(SocketError) .and_raise(SocketError)
end end
it { is_expected.to eq(connection_status: :unreachable, nodes: []) } it { is_expected.to eq(connection_status: :unreachable, nodes: nil) }
end end
context 'cluster cannot be authenticated to' do context 'cluster cannot be authenticated to' do
...@@ -1066,7 +1066,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do ...@@ -1066,7 +1066,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(OpenSSL::X509::CertificateError.new("Certificate error")) .and_raise(OpenSSL::X509::CertificateError.new("Certificate error"))
end end
it { is_expected.to eq(connection_status: :authentication_failure, nodes: []) } it { is_expected.to eq(connection_status: :authentication_failure, nodes: nil) }
end end
describe 'Kubeclient::HttpError' do describe 'Kubeclient::HttpError' do
...@@ -1078,18 +1078,18 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do ...@@ -1078,18 +1078,18 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(Kubeclient::HttpError.new(error_code, error_message, nil)) .and_raise(Kubeclient::HttpError.new(error_code, error_message, nil))
end end
it { is_expected.to eq(connection_status: :authentication_failure, nodes: []) } it { is_expected.to eq(connection_status: :authentication_failure, nodes: nil) }
context 'generic timeout' do context 'generic timeout' do
let(:error_message) { 'Timed out connecting to server'} let(:error_message) { 'Timed out connecting to server'}
it { is_expected.to eq(connection_status: :unreachable, nodes: []) } it { is_expected.to eq(connection_status: :unreachable, nodes: nil) }
end end
context 'gateway timeout' do context 'gateway timeout' do
let(:error_message) { '504 Gateway Timeout for GET https://kubernetes.example.com/api/v1'} let(:error_message) { '504 Gateway Timeout for GET https://kubernetes.example.com/api/v1'}
it { is_expected.to eq(connection_status: :unreachable, nodes: []) } it { is_expected.to eq(connection_status: :unreachable, nodes: nil) }
end end
end end
...@@ -1099,7 +1099,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do ...@@ -1099,7 +1099,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(StandardError) .and_raise(StandardError)
end end
it { is_expected.to eq(connection_status: :unknown_failure, nodes: []) } it { is_expected.to eq(connection_status: :unknown_failure, nodes: nil) }
it 'notifies Sentry' do it 'notifies Sentry' do
expect(Gitlab::ErrorTracking).to receive(:track_exception) expect(Gitlab::ErrorTracking).to receive(:track_exception)
......
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