Cluster dashboard overview feedback

Add exact to type (link) component

Live data format for events

Pretty cluster providers

Add ago suffix to LiveDate

tweak events headers

remove unneeded suffix

change to unhealthy nodes

only show constraints with violations on overview

ignore tern port files

overview detail gatekeeper disabled bugs

Update percentage bar to take in percentage value

tweak exponents and fix issue wiht max/min

change cluster events default sort

update comments for defaultSortby

cluster infobox feedback

Pass nodes down to InfoBoxCluster to parse only worker nodes info

fix engine config translations

node counts infobox cluster

alignment issues

review feedback

lint warnings
This commit is contained in:
Westly Wright 2020-03-30 10:41:38 -07:00
parent d9d04995e3
commit 63779a1d1a
No known key found for this signature in database
GPG Key ID: 4FAB3D8673DC54A3
18 changed files with 521 additions and 192 deletions

1
.gitignore vendored
View File

@ -83,6 +83,7 @@ dist
.idea
.editorconfig
*.org
*.tern-port
# Service worker
sw.*

View File

@ -81,6 +81,23 @@ $GUTTER: 1.75%;
}
}
.flex-justify-center {
justify-content: center;
}
.flex-justify-right {
justify-content: right;
}
.flex-justify-left {
justify-content: left;
}
.container-flex-center {
@extend .container-flex;
align-items: center;
}
// Equal height columns
.row-full-height {
height: 100%;

View File

@ -16,6 +16,18 @@ nav:
namespace: "{name}"
orphan: "Not in a Project"
cluster:
provider:
aliyunengineconfig: Alibaba ACK
amazonelasticcontainerserviceconfig: Amazon EKS
azurekubernetesserviceconfig: Azure AKS
googlekubernetesengineconfig: Google GKE
huaweiengineconfig: Huawei CCE
importedconfig: Import
rancherkubernetesengineconfig: Rancher RKE
tencentengineconfig: Tencent TKE
baiduengineconfig: Baidu CCE
footer:
docs: Docs
forums: Forums
@ -27,9 +39,13 @@ clusterIndexPage:
header: "Cluster: {name}"
sections:
nodes:
label: Nodes
label: Unhealthy Nodes
noRows: "There are no unhealthy nodes to show."
gatekeeper:
label: OPA Gatekeeper Constraints
label: OPA Gatekeeper Constraint Violations
disabled: OPA Gatekeeper is not configured.
buttonText: Configure Gatekeeper
noRows: "There are no contrains with violations to show."
events:
label: Events
@ -39,11 +55,20 @@ infoBoxCluster:
memory: Memory
pods: Pods
provider: Provider
reserved: "{numerator} of {denominator} Reserved"
used: "{numerator} of {denominator} Used"
reserved: "{numerator} of {denominator} rsvd"
used: "{numerator} of {denominator} used"
version: Kubernetes Version
nodes:
worker:
label: Worker Nodes
etcd:
label: Etcd Nodes
controlPlane:
label: Control Plane Nodes
sortableTable:
noRows: "There are no rows to show."
noData: "There are no rows which match your search query."
paging:
generic: |
{pages, plural,

View File

@ -375,7 +375,7 @@ export default {
<div class="col span-6">
<h3>Description</h3>
<ul>
<li>OPA Gatekeeper provides first-class integration between OPA (Open Policy Agent) and Kubernetes.</li>
<li>OPA Gatekeeper provides first-class integration between OPA (Open Policy Agent) and Kubernetes.</li>
<li>You can Customize Gatekeepers yaml configuartion or Enable Gatekeeper with defaults.</li>
<li>For more information, visit the <a href="https://www.openpolicyagent.org/docs/latest/kubernetes-introduction/" target="blank">OPA documentation.</a></li>
</ul>
@ -389,7 +389,7 @@ export default {
</div>
</div>
<div>
<div class="spacer"></div>
<div class="spacer"></div>
<div v-if="!showYamlEditor" class="text-center">
<button
type="button"

View File

@ -9,33 +9,40 @@ export default {};
</template>
<style lang="scss" scoped>
.info-box {
background-color: var(--tabbed-container-bg);
border: 1px solid var(--tabbed-border);
padding: 20px;
border-radius: var(--border-radius);
.info-column:not(:last-child) {
border-right: 1px solid var(--tabbed-border);
}
.info-row {
margin-bottom: 10px;
}
.info-column,
.info-row {
label {
color: var(--input-label);
.info-box {
background-color: var(--tabbed-container-bg);
border: 1px solid var(--tabbed-border);
padding: 20px;
border-radius: var(--border-radius);
.info-column:not(:last-child) {
border-right: 1px solid var(--tabbed-border);
}
}
.info-column {
.usage {
.info-row {
margin-bottom: 10px;
label {
.info-row-label {
padding-top: 2px;
display: inline-block;
font-size: 12px;
padding-top: 6px;
}
}
.info-column,
.info-row {
label {
color: var(--input-label);
}
}
.info-column {
.usage {
margin-bottom: 10px;
label {
display: inline-block;
font-size: 12px;
padding-top: 6px;
}
}
.flex-item-half {
align-items: center;
}
}
}
}
</style>

View File

@ -2,7 +2,22 @@
import { isEmpty } from 'lodash';
import InfoBox from '@/components/InfoBox';
import PercentageBar from '@/components/PercentageBar';
import { parseSi, formatSi } from '@/utils/units';
import { parseSi, formatSi, exponentNeeded } from '@/utils/units';
const PARSE_RULES = {
memory: {
format: {
addSuffix: true,
firstSuffix: 'B',
increment: 1000,
maxExponent: 99,
maxPrecision: 2,
minExponent: 0,
startingExponent: 0,
suffix: 'iB',
}
}
};
export default {
components: {
@ -18,62 +33,118 @@ export default {
metrics: {
type: Array,
required: true,
},
nodes: {
type: Array,
required: true,
default: () => [],
}
},
computed: {
nodeCounts() {
const nodes = this.nodes;
const countsOut = {
workerNodes: nodes.filter(n => n.isWorker).length || 0,
etcdNodes: nodes.filter(n => n.isEtcd).length || 0,
controlPlaneNodes: nodes.filter(n => n.isControlPlane).length || 0,
};
return countsOut;
},
liveNodeUsage() {
const clusterCapacityCpu = this.cluster?.status?.capacity?.cpu;
return this.getUsage(clusterCapacityCpu, 'cpu');
},
nodeUsageReserved() {
const allocatableCpu = this.cluster?.status?.allocatable?.cpu;
return this.getUsage(allocatableCpu, 'cpu');
return this.getLiveUsage(clusterCapacityCpu, 'cpu');
},
liveNodeMemUsage() {
const clusterCapacityMemory = this.cluster?.status?.capacity?.memory;
const { memory: { format } } = PARSE_RULES;
return this.getUsage(clusterCapacityMemory, 'memory');
return this.getLiveUsage(clusterCapacityMemory, 'memory', format);
},
nodeUsageReserved() {
const requested = this.cluster?.status?.requested?.cpu;
const allocatable = this.cluster?.status?.allocatable?.cpu;
return this.getReservedUsage(requested, allocatable);
},
nodeUsageMemReserved() {
const allocatableMem = this.cluster?.status?.allocatable?.memory;
const requested = this.cluster?.status?.requested?.memory;
const allocatable = this.cluster?.status?.allocatable?.memory;
const { memory: { format } } = PARSE_RULES;
return this.getUsage(allocatableMem, 'memory');
format.increment = 1024;
return this.getReservedUsage(requested, allocatable, format);
},
nodeUsagePodReserved() {
const { memory: { format } } = PARSE_RULES;
const podCapacity = parseSi(this.cluster?.status?.allocatable?.pods || 0);
const podUsage = parseSi(this.cluster?.status?.requested?.pods || 0);
return {
clusterCapacity: formatSi(podCapacity),
nodeUsage: formatSi(podUsage),
clusterCapacity: formatSi(podCapacity, { ...format, ...{ addSuffix: false } }),
nodeUsage: formatSi(podUsage, { ...format, ...{ addSuffix: false } }),
percentage: podCapacity === 0 ? 0 : ( podUsage * 100 ) / podCapacity,
};
},
},
methods: {
getUsage(capacity = 0, field = 'cpu') {
const metrics = this.metrics;
getReservedUsage(requested = 0, allocatable = 0, formatOpts = {}) {
const parsed = {
requested: parseSi(requested),
allocatable: parseSi(allocatable),
};
const percentage = parsed.allocatable === 0 ? parsed.allocatable : ( parsed.requested * 100 ) / parsed.allocatable;
formatOpts = {
...formatOpts,
...{
maxExponent: exponentNeeded(parsed.allocatable),
minExponent: exponentNeeded(parsed.allocatable),
}
};
return {
requested: formatSi(parsed.requested, { ...formatOpts, ...{ addSuffix: false } }),
allocatable: formatSi(parsed.allocatable, formatOpts),
percentage
};
},
getLiveUsage(capacity, field = 'cpu', formatOpts = {}) {
const nodes = this.nodes;
const metrics = this.metrics.filter(n => nodes.find(nd => nd.id === n.id && nd.isWorker));
const normalizedCapacity = parseSi(capacity);
const nodesEachUsage = metrics.map( m => parseSi(m.usage[field]));
const cumulativeUsage = isEmpty(nodesEachUsage) ? 0 : nodesEachUsage.reduce( ( acc, cv ) => acc + cv);
let formatedCapacity = formatSi(capacity);
if (isNaN(formatedCapacity)) {
formatedCapacity = formatSi(parseSi(capacity));
if (field === 'memory') {
formatOpts = {
...formatOpts,
...{
maxExponent: exponentNeeded(normalizedCapacity),
minExponent: exponentNeeded(normalizedCapacity),
increment: 1024,
}
};
}
const formatedCapacity = formatSi(normalizedCapacity, formatOpts);
const formatedUsage = formatSi(cumulativeUsage, { ...formatOpts, ...{ addSuffix: false } });
const percentage = normalizedCapacity === 0 ? normalizedCapacity : ( cumulativeUsage * 100 ) / normalizedCapacity;
return {
clusterCapacity: formatedCapacity,
nodeUsage: formatSi(cumulativeUsage),
percentage: normalizedCapacity === 0 ? normalizedCapacity : ( cumulativeUsage * 100 ) / normalizedCapacity,
nodeUsage: formatedUsage,
percentage,
};
},
}
@ -84,39 +155,51 @@ export default {
<InfoBox class="row">
<div class="col span-3 info-column">
<div class="info-row">
<label><t k="infoBoxCluster.provider" /></label>
<br />
{{ cluster.displayProvider }}
<label class="info-row-label">
<t k="infoBoxCluster.provider" />
</label>
<div>
{{ cluster.displayProvider }}
</div>
</div>
<div class="info-row">
<label><t k="infoBoxCluster.version" /></label>
<br />
{{ cluster.kubernetesVersion }}
<label class="info-row-label">
<t k="infoBoxCluster.version" />
</label>
<div>
{{ cluster.displayProvider }}
</div>
</div>
<div class="info-row">
<label><t k="infoBoxCluster.created" /></label>
<br />
<LiveDate :value="cluster.metadata.creationTimestamp" />
<label class="info-row-label">
<t k="infoBoxCluster.created" />
</label>
<div>
<LiveDate
:value="cluster.metadata.creationTimestamp"
:add-suffix="true"
/>
</div>
</div>
</div>
<div class="col span-3 info-column">
<label class="mb-10"><t k="infoBoxCluster.cpu" /></label>
<div class="container-flex">
<div class="flex-item-half usage">
<div class="container-flex-center mb-10">
<div class="flex-item-half">
<label>
<t
k="infoBoxCluster.reserved"
:numerator="nodeUsageReserved.nodeUsage"
:denominator="nodeUsageReserved.clusterCapacity"
:numerator="nodeUsageReserved.requested"
:denominator="nodeUsageReserved.allocatable"
/>
</label>
</div>
<div class="flex-item-half">
<PercentageBar :value="nodeUsageReserved.percentage" />
<div class="flex-item-half flex-justify-center">
<PercentageBar class="container-flex-center" :value="nodeUsageReserved.percentage" />
</div>
</div>
<div class="container-flex">
<div class="flex-item-half usage">
<div class="container-flex-center mb-10">
<div class="flex-item-half">
<label>
<t
k="infoBoxCluster.used"
@ -125,29 +208,29 @@ export default {
/>
</label>
</div>
<div class="flex-item-half">
<PercentageBar :value="liveNodeUsage.percentage" />
<div class="flex-item-half flex-justify-center">
<PercentageBar class="container-flex-center" :value="liveNodeUsage.percentage" />
</div>
</div>
</div>
<div class="col span-3 info-column">
<label class="mb-10"><t k="infoBoxCluster.memory" /></label>
<div class="container-flex">
<div class="flex-item-half usage">
<div class="container-flex-center mb-10">
<div class="flex-item-half">
<label>
<t
k="infoBoxCluster.reserved"
:numerator="nodeUsageMemReserved.nodeUsage"
:denominator="nodeUsageMemReserved.clusterCapacity"
:numerator="nodeUsageMemReserved.requested"
:denominator="nodeUsageMemReserved.allocatable"
/>
</label>
</div>
<div class="flex-item-half">
<PercentageBar :value="nodeUsageMemReserved.percentage" />
<div class="flex-item-half flex-justify-center">
<PercentageBar class="container-flex-center" :value="nodeUsageMemReserved.percentage" />
</div>
</div>
<div class="container-flex">
<div class="flex-item-half usage">
<div class="container-flex-center mb-10">
<div class="flex-item-half">
<label>
<t
k="infoBoxCluster.used"
@ -156,25 +239,63 @@ export default {
/>
</label>
</div>
<div class="flex-item-half">
<PercentageBar :value="liveNodeMemUsage.percentage" />
<div class="flex-item-half flex-justify-center">
<PercentageBar class="container-flex-center" :value="liveNodeMemUsage.percentage" />
</div>
</div>
</div>
<div class="col span-3 info-column">
<label class="mb-10"><t k="infoBoxCluster.pods" /></label>
<div class="container-flex">
<div class="flex-item-half usage">
<div>
<label class="mb-10"><t k="infoBoxCluster.pods" /></label>
<div class="container-flex-center mb-10">
<div class="flex-item-half">
<label>
<t
k="infoBoxCluster.reserved"
:numerator="nodeUsagePodReserved.nodeUsage"
:denominator="nodeUsagePodReserved.clusterCapacity"
/>
</label>
</div>
<div class="flex-item-half flex-justify-center">
<PercentageBar :value="nodeUsagePodReserved.percentage" />
</div>
</div>
</div>
<div class="container-flex-center mb-10">
<div class="flex-item-half">
<label>
<t
k="infoBoxCluster.reserved"
:numerator="nodeUsagePodReserved.nodeUsage"
:denominator="nodeUsagePodReserved.clusterCapacity"
/>
<t k="infoBoxCluster.nodes.worker.label" />:
</label>
</div>
<div class="flex-item-half flex-justify-center">
<label>
{{ nodeCounts.workerNodes }}
</label>
</div>
</div>
<div class="container-flex-center mb-10">
<div class="flex-item-half">
<PercentageBar :value="nodeUsagePodReserved.percentage" />
<label>
<t k="infoBoxCluster.nodes.etcd.label" />:
</label>
</div>
<div class="flex-item-half flex-justify-center">
<label>
{{ nodeCounts.etcdNodes }}
</label>
</div>
</div>
<div class="container-flex-center mb-10">
<div class="flex-item-half">
<label>
<t k="infoBoxCluster.nodes.controlPlane.label" />:
</label>
</div>
<div class="flex-item-half flex-justify-center">
<label>
{{ nodeCounts.controlPlaneNodes }}
</label>
</div>
</div>
</div>

View File

@ -6,14 +6,11 @@ import { formatPercent } from '@/utils/string';
export default {
props: {
/**
* A value representing the percentage to be displayed. *Must be a value between 0 and 1*.
* A value representing the percentage to be displayed. *Must be a value between 0 and 100*.
*/
value: {
type: Number,
required: true,
validator: (value) => {
return value >= 0 && value <= 1;
}
}
},
data() {
@ -26,7 +23,7 @@ export default {
},
methods: {
getTickBackgroundClass(i) {
const valuePercentage = this.value;
const valuePercentage = ( this.value / 100 );
const barPercentage = i / this.NUMBER_OF_TICKS;
if (valuePercentage < barPercentage) {
@ -58,17 +55,17 @@ export default {
<style lang='scss'>
.percentage {
vertical-align: middle;
vertical-align: middle;
}
.bar {
vertical-align: middle;
margin-left: 3px;
vertical-align: middle;
margin-left: 3px;
.tick {
display: inline-block;
overflow: hidden;
margin-right: 3px;
width: 3px;
font-size: 1.2em;
display: inline-block;
overflow: hidden;
margin-right: 3px;
width: 3px;
font-size: 1.2em;
}
}
</style>

View File

@ -30,7 +30,7 @@ export default {
props: {
headers: {
// {
// name: Name for the column (goes in query param)
// name: Name for the column (goes in query param) and for defaultSortBy
// label: Displayed column header
// sort: string|array[string] Field name(s) to sort by, default: [name, keyField]
// fields can be suffixed with ':desc' to flip the normal sort order
@ -69,6 +69,7 @@ export default {
defaultSortBy: {
// Default field to sort by if none is specified
// uses name on headers
type: String,
default: null
},
@ -84,6 +85,7 @@ export default {
type: Boolean,
default: true
},
rowActionsWidth: {
// How wide the action dropdown column should be
type: Number,
@ -95,6 +97,7 @@ export default {
type: Boolean,
default: true
},
extraSearchFields: {
// Additional fields that aren't defined in the headers to search in on each row
type: Array,
@ -186,6 +189,23 @@ export default {
type: Number,
default: null, // Default comes from the user preference
},
/**
* Allows you to override the default translation text of no rows view
*/
noRowsKey: {
type: String,
default: 'sortableTable.noRows'
},
/**
* Allows you to override the default translation text of no search data view
*/
noDataKey: {
type: String,
default: 'sortableTable.noData'
}
},
data() {
@ -351,8 +371,7 @@ export default {
</div>
</div>
<table class="sortable-table" :class="classObject" width="100%">
<thead
is="THead"
<THead
:columns="columns"
:table-actions="tableActions"
:row-actions="rowActions"
@ -370,7 +389,7 @@ export default {
<slot name="no-rows">
<tr>
<td :colspan="fullColspan" class="no-rows">
There are no rows to show.
<t :k="noRowsKey" />
</td>
</tr>
</slot>
@ -379,7 +398,7 @@ export default {
<slot name="no-results">
<tr>
<td :colspan="fullColspan" class="no-results text-center">
There are no rows which match your search query.
<t :k="noDataKey" />
</td>
</tr>
</slot>
@ -421,7 +440,7 @@ export default {
:value="valueFor(row,col)"
:row="row"
:col="col"
:opts="col.formatterOpts"
v-bind="col.formatterOpts"
/>
<template v-else>
{{ valueFor(row,col) }}

View File

@ -12,7 +12,15 @@ export default {
value: {
type: String,
default: ''
}
},
addSuffix: {
type: Boolean,
default: false,
},
suffix: {
type: String,
default: 'ago',
},
},
data() {
@ -32,6 +40,16 @@ export default {
return out;
},
suffixedLabel() {
let out = `${ this.label }`;
if (this.addSuffix) {
out = `${ out } ${ this.suffix }`;
}
return out;
},
},
watch: {
@ -102,6 +120,6 @@ export default {
<template>
<span v-tooltip="title">
{{ label }}
{{ suffixedLabel }}
</span>
</template>

View File

@ -53,6 +53,7 @@ export default {
:to="type.route"
tag="li"
class="child"
:exact="type.exact"
>
<a
@mouseenter="setNear(true)"

View File

@ -203,6 +203,7 @@ export default function(store) {
group: 'Cluster',
weight: 11,
route: { name: 'c-cluster' },
exact: true,
});
virtualType({

View File

@ -78,3 +78,34 @@ export const METRIC = {
NODE: 'metrics.k8s.io.nodemetrics',
POD: 'metrics.k8s.io.podmetrics',
};
export const GATEKEEPER = {
TEMPLATE_ID: 'cattle-global-data/system-library-rancher-gatekeeper-operator',
APP_ID: 'rancher-gatekeeper-operator',
CONFIG: `---
replicas: 1
auditInterval: 300
constraintViolationsLimit: 20
auditFromCache: false
image:
repository: rancher/opa-gatekeeper
tag: v3.1.0-beta.7
pullPolicy: IfNotPresent
nodeSelector: {"beta.kubernetes.io/os": "linux"}
tolerations: []
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
global:
systemDefaultRegistry: ""
kubectl:
repository: rancher/istio-kubectl
tag: 1.4.6
`
};
export const SYSTEM_PROJECT_LABEL = 'authz.management.cattle.io/system-project';

13
models/event.js Normal file
View File

@ -0,0 +1,13 @@
import { isEmpty } from 'lodash';
export default {
displayInvolvedObject() {
const involvedObject = this.involvedObject;
if (isEmpty(involvedObject)) {
return 'N/A';
}
return `${ involvedObject.kind } - ${ involvedObject.name }`;
},
};

View File

@ -6,17 +6,29 @@ export default {
return this.metadata.name;
},
roles() {
const {
CONTROL_PLANE: controlPlane,
WORKER: worker,
ETCD: etcd
} = NODE_ROLES;
isWorker() {
const { WORKER: worker } = NODE_ROLES;
const labels = this.metadata?.labels;
const isControlPlane = labels[controlPlane];
const isWorker = labels[worker];
const isEtcd = labels[etcd];
return labels[worker] && labels[worker].toLowerCase() === 'true';
},
isControlPlane() {
const { CONTROL_PLANE: controlPlane } = NODE_ROLES;
const labels = this.metadata?.labels;
return labels[controlPlane] && labels[controlPlane].toLowerCase() === 'true';
},
isEtcd() {
const { ETCD: etcd } = NODE_ROLES;
const labels = this.metadata?.labels;
return labels[etcd] && labels[etcd].toLowerCase() === 'true';
},
roles() {
const { isControlPlane, isWorker, isEtcd } = this;
if (( isControlPlane && isWorker && isEtcd ) ||
( !isControlPlane && !isWorker && !isEtcd )) {

View File

@ -1,39 +1,16 @@
<script>
import { NAMESPACE, MANAGEMENT, EXTERNAL } from '@/config/types';
import {
NAMESPACE,
MANAGEMENT,
EXTERNAL,
GATEKEEPER,
SYSTEM_PROJECT_LABEL,
} from '@/config/types';
import GatekeeperConfig from '@/components/GatekeeperConfig';
import { _CREATE, _EDIT, _VIEW } from '@/config/query-params';
import InfoBox from '@/components/InfoBox';
import GatekeeperViolationsTable from '@/components/GatekeeperViolationsTable';
const TEMPLATE_ID = 'cattle-global-data/system-library-rancher-gatekeeper-operator';
const APP_ID = 'rancher-gatekeeper-operator';
const CONFIG = `---
replicas: 1
auditInterval: 300
constraintViolationsLimit: 20
auditFromCache: false
image:
repository: rancher/opa-gatekeeper
tag: v3.1.0-beta.7
pullPolicy: IfNotPresent
nodeSelector: {"beta.kubernetes.io/os": "linux"}
tolerations: []
resources:
limits:
cpu: 1000m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
global:
systemDefaultRegistry: ""
kubectl:
repository: rancher/istio-kubectl
tag: 1.4.6
`;
const SYSTEM_PROJECT_LABEL = 'authz.management.cattle.io/system-project';
export default {
components: {
GatekeeperConfig, GatekeeperViolationsTable, InfoBox
@ -54,7 +31,7 @@ export default {
try {
const template = await store.dispatch('management/find', {
type: MANAGEMENT.CATALOG_TEMPLATE,
id: TEMPLATE_ID
id: GATEKEEPER.TEMPLATE_ID
});
if (!template?.id ) {
@ -91,7 +68,7 @@ export default {
// @TODO externalCluster service doesn't like getting things by ID, so load them all and find it...
const apps = await store.dispatch('clusterExternal/findAll', { type: EXTERNAL.APP });
let gatekeeper = apps.find(app => app.id === `${ namespace }/${ APP_ID }`);
let gatekeeper = apps.find(app => app.id === `${ namespace }/${ GATEKEEPER.APP_ID }`);
const namespaces = await store.dispatch('cluster/findAll', { type: NAMESPACE });
@ -105,14 +82,14 @@ export default {
type: 'app',
metadata: {
namespace,
name: APP_ID
name: GATEKEEPER.APP_ID
},
spec: {
projectName: targetSystemProject.namespacedName,
externalId: latestGKVersion.externalId,
targetNamespace: 'gatekeeper-system',
timeout: 300,
valuesYaml: CONFIG,
valuesYaml: GATEKEEPER.CONFIG,
}
});
}
@ -183,12 +160,12 @@ export default {
type: 'app',
metadata: {
namespace: systemProject.metadata.name,
name: APP_ID
name: GATEKEEPER.APP_ID
},
spec: {
targetNamespace: 'gatekeeper-system',
timeout: 300,
valuesYaml: CONFIG,
valuesYaml: GATEKEEPER.CONFIG,
projectName: systemProject.namespacedName,
externalId,
}

View File

@ -1,5 +1,5 @@
<script>
// import { isEmpty } from 'lodash';
import { isEmpty } from 'lodash';
import { get } from '@/utils/object';
import InfoBoxCluster from '@/components/InfoBoxCluster';
import InfoBox from '@/components/InfoBox';
@ -14,7 +14,15 @@ import {
} from '@/config/table-headers';
import { DESCRIPTION } from '@/config/labels-annotations';
import { findAllConstraints } from '@/utils/gatekeeper/util';
import { MANAGEMENT, EVENT, NODE, METRIC } from '@/config/types';
import {
MANAGEMENT,
EVENT,
NODE,
METRIC,
EXTERNAL,
GATEKEEPER,
SYSTEM_PROJECT_LABEL,
} from '@/config/types';
import { allHash } from '@/utils/promise';
export default {
@ -28,7 +36,7 @@ export default {
const constraintHeaders = [
NAME,
{
name: 'Violations',
name: 'violations',
label: 'Violations',
value: 'status.totalViolations',
sort: 'status.totalViolations',
@ -37,25 +45,28 @@ export default {
STATE,
];
const reason = { ...REASON, ...{ width: 100 } };
const reason = { ...REASON, ...{ canBeVariable: true } };
const message = { ...MESSAGE, ...{ canBeVariable: true } };
const eventHeaders = [
NAMESPACE_NAME,
reason,
{
name: 'Object',
label: 'Object',
value: 'involvedObject.kind',
sort: 'involvedObject.kind',
width: 100
name: 'object',
label: 'Object',
value: 'displayInvolvedObject',
sort: ['involvedObject.kind', 'involvedObject.name'],
canBeVariable: true,
formatter: 'LinkDetail',
},
MESSAGE,
message,
{
name: 'Date',
label: 'Date',
value: 'lastTimestamp',
sort: 'lastTimestamp',
formatter: 'Date',
width: 125
align: 'center',
name: 'date',
label: 'Date',
value: 'lastTimestamp',
sort: 'lastTimestamp',
formatter: 'LiveDate',
formatterOpts: { addSuffix: true },
width: 125
},
];
@ -66,16 +77,57 @@ export default {
];
return {
pollingTimeoutId: null,
pollingTimeoutId: null,
gatekeeperEnabled: false,
constraintHeaders,
eventHeaders,
nodeHeaders,
};
},
computed: {
filteredNodes() {
const allNodes = ( this.nodes || [] ).slice();
return allNodes.filter(node => !node.state.includes('healthy') && !node.state.includes('active'));
},
filteredConstraints() {
const allConstraints = ( this.constraints || [] ).slice();
return allConstraints.filter(constraint => constraint.status.totalViolations > 0);
},
},
async asyncData(ctx) {
const { route, store } = ctx;
const id = get(route, 'params.cluster');
let gatekeeper = null;
let gatekeeperEnabled = false;
const projects = await store.dispatch('clusterExternal/findAll', { type: EXTERNAL.PROJECT });
const targetSystemProject = projects.find(( proj ) => {
const labels = proj.metadata?.labels || {};
if ( labels[SYSTEM_PROJECT_LABEL] === 'true' ) {
return true;
}
});
if (!isEmpty(targetSystemProject)) {
const systemNamespace = targetSystemProject.metadata.name;
try {
gatekeeper = await store.dispatch('clusterExternal/find', {
type: EXTERNAL.APP,
id: `${ systemNamespace }/${ GATEKEEPER.APP_ID }`,
});
if (!isEmpty(gatekeeper)) {
gatekeeperEnabled = true;
}
} catch (err) {
gatekeeperEnabled = false;
}
}
const cluster = await store.dispatch('management/find', { type: MANAGEMENT.CLUSTER, id });
@ -86,6 +138,7 @@ export default {
nodeMetrics: [],
pollingErrorCount: 0,
cluster,
gatekeeperEnabled,
};
},
@ -102,7 +155,11 @@ export default {
},
mounted() {
this.pollMetrics();
const schema = this.$store.getters['cluster/schemaFor'](METRIC.NODE);
if (schema) {
this.pollMetrics();
}
},
methods: {
@ -132,15 +189,21 @@ export default {
},
async fetchClusterResources(type, opt = {}) {
try {
const resources = await this.$store.dispatch('cluster/findAll', { type, opt });
const schema = this.$store.getters['cluster/schemaFor'](type);
return resources;
} catch (err) {
console.error(`Failed fetching cluster resource ${ type } with error:`, err);
if (schema) {
try {
const resources = await this.$store.dispatch('cluster/findAll', { type, opt });
return [];
return resources;
} catch (err) {
console.error(`Failed fetching cluster resource ${ type } with error:`, err);
return [];
}
}
return [];
},
async pollMetrics() {
@ -195,6 +258,7 @@ export default {
<InfoBoxCluster
:cluster="cluster"
:metrics="nodeMetrics"
:nodes="nodes"
/>
<div class="row">
<div class="col span-6 equal-height">
@ -204,12 +268,13 @@ export default {
</label>
<div class="row mt-10">
<SortableTable
:rows="nodes"
:rows="filteredNodes"
:headers="nodeHeaders"
key-field="id"
:search="false"
:table-actions="false"
:row-actions="false"
no-rows-key="clusterIndexPage.sections.nodes.noRows"
key-field="id"
/>
</div>
</InfoBox>
@ -219,15 +284,36 @@ export default {
<label>
<t k="clusterIndexPage.sections.gatekeeper.label" />
</label>
<div class="row mt-10">
<SortableTable
:rows="constraints"
:headers="constraintHeaders"
key-field="id"
:search="false"
:table-actions="false"
:row-actions="false"
/>
<div v-if="gatekeeperEnabled">
<div class="row mt-10">
<SortableTable
:rows="filteredConstraints"
:headers="constraintHeaders"
:search="false"
:table-actions="false"
:row-actions="false"
key-field="id"
no-rows-key="clusterIndexPage.sections.gatekeeper.noRows"
/>
</div>
</div>
<div v-else>
<hr class="mt-35 mb-10" />
<div class="mt-35 mb-35 text-center">
<div>
<t k="clusterIndexPage.sections.gatekeeper.disabled" />
</div>
<n-link
:to="{ name: 'c-cluster-gatekeeper' }"
role="link"
type="button"
class="btn role-link"
>
<a>
<t k="clusterIndexPage.sections.gatekeeper.buttonText" />
</a>
</n-link>
</div>
</div>
</InfoBox>
</div>
@ -248,6 +334,7 @@ export default {
:row-actions="false"
:paging="true"
:rows-per-page="10"
default-sort-by="date"
/>
</div>
</InfoBox>

View File

@ -375,6 +375,7 @@ export const getters = {
labelDisplay,
mode: typeObj.mode,
count,
exact: typeObj.exact || false,
namespaced,
route,
name: typeObj.name,

View File

@ -1,5 +1,5 @@
export const UNITS = ['', 'K', 'M', 'G', 'T', 'P'];
export const FRACTIONAL = ['', 'm', 'u', 'n', 'p', 'f']; // milli micro nano pico femto
export const UNITS = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
export const FRACTIONAL = ['', 'm', 'u', 'n', 'p', 'f', 'a', 'z', 'y']; // milli micro nano pico femto
export function formatSi(inValue, {
increment = 1000,
@ -14,7 +14,8 @@ export function formatSi(inValue, {
let val = inValue;
let exp = startingExponent;
while ( val >= increment && (exp + 1 < UNITS.length || exp < minExponent ) && (exp < maxExponent)) {
// TODO More to think about re: min > max
while ( ( val >= increment && exp + 1 < UNITS.length && exp < maxExponent ) || exp < minExponent ) {
val = val / increment;
exp++;
}