mirror of https://github.com/rancher/dashboard.git
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:
parent
d9d04995e3
commit
63779a1d1a
|
|
@ -83,6 +83,7 @@ dist
|
|||
.idea
|
||||
.editorconfig
|
||||
*.org
|
||||
*.tern-port
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export default {
|
|||
:to="type.route"
|
||||
tag="li"
|
||||
class="child"
|
||||
:exact="type.exact"
|
||||
>
|
||||
<a
|
||||
@mouseenter="setNear(true)"
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ export default function(store) {
|
|||
group: 'Cluster',
|
||||
weight: 11,
|
||||
route: { name: 'c-cluster' },
|
||||
exact: true,
|
||||
});
|
||||
|
||||
virtualType({
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 }`;
|
||||
},
|
||||
};
|
||||
|
|
@ -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 )) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -375,6 +375,7 @@ export const getters = {
|
|||
labelDisplay,
|
||||
mode: typeObj.mode,
|
||||
count,
|
||||
exact: typeObj.exact || false,
|
||||
namespaced,
|
||||
route,
|
||||
name: typeObj.name,
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue