dashboard/detail/provisioning.cattle.io.clus...

518 lines
16 KiB
Vue

<script>
import Loading from '@/components/Loading';
import Banner from '@/components/Banner';
import ResourceTable from '@/components/ResourceTable';
import ResourceTabs from '@/components/form/ResourceTabs';
import SortableTable from '@/components/SortableTable';
import CopyCode from '@/components/CopyCode';
import Tab from '@/components/Tabbed/Tab';
import { allHash } from '@/utils/promise';
import { CAPI, MANAGEMENT, NORMAN } from '@/config/types';
import {
STATE, NAME as NAME_COL, AGE, AGE_NORMAN, STATE_NORMAN, ROLES,
} from '@/config/table-headers';
import CustomCommand from '@/edit/provisioning.cattle.io.cluster/CustomCommand';
import AsyncButton from '@/components/AsyncButton.vue';
export default {
components: {
Loading,
Banner,
ResourceTable,
ResourceTabs,
SortableTable,
Tab,
CopyCode,
CustomCommand,
AsyncButton,
},
props: {
value: {
type: Object,
default: () => {
return {};
}
}
},
async fetch() {
await this.value.waitForProvisioner();
const hash = {};
if (this.value.isImported || this.value.isRke1) {
// Cluster isn't compatible with machines/machineDeployments, show nodes/node pools instead
if ( this.$store.getters['management/canList'](MANAGEMENT.NODE) ) {
hash.allNodes = this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE });
}
if ( this.$store.getters['management/canList'](MANAGEMENT.NODE_POOL) ) {
hash.allNodePools = this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE_POOL });
}
if ( this.$store.getters['management/canList'](MANAGEMENT.NODE_TEMPLATE) ) {
hash.nodeTemplates = this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE_TEMPLATE });
}
} else {
if ( this.$store.getters['management/canList'](CAPI.MACHINE_DEPLOYMENT) ) {
hash.machineDeployments = this.$store.dispatch('management/findAll', { type: CAPI.MACHINE_DEPLOYMENT });
}
if ( this.$store.getters['management/canList'](CAPI.MACHINE) ) {
hash.machines = this.$store.dispatch('management/findAll', { type: CAPI.MACHINE });
}
}
if (this.value.isImported || this.value.isCustom) {
hash.clusterToken = this.value.getOrCreateToken();
}
if ( this.value.isRke1 && this.$store.getters['isRancher'] ) {
hash.etcdBackups = this.$store.dispatch('rancher/findAll', { type: NORMAN.ETCD_BACKUP });
hash.normanNodePools = this.$store.dispatch('rancher/findAll', { type: NORMAN.NODE_POOL });
}
const res = await allHash(hash);
this.allMachines = res.machines || [];
this.allMachineDeployments = res.machineDeployments || [];
this.allNodes = res.allNodes || [];
this.allNodePools = res.allNodePools || [];
this.haveMachines = !!res.machines;
this.haveDeployments = !!res.machineDeployments;
this.haveNodePools = !!res.allNodePools;
this.haveNodes = !!res.allNodes;
this.clusterToken = res.clusterToken;
this.etcdBackups = res.etcdBackups;
const machineDeloymentTemplateType = res.machineDeployments?.[0]?.templateType;
if (machineDeloymentTemplateType && this.$store.getters['management/schemaFor'](machineDeloymentTemplateType) ) {
await this.$store.dispatch('management/findAll', { type: machineDeloymentTemplateType });
}
},
data() {
return {
allMachines: [],
allMachineDeployments: [],
allNodes: [],
allNodePools: [],
haveMachines: false,
haveDeployments: false,
haveNodes: false,
haveNodePools: false,
mgmtNodeSchema: this.$store.getters[`management/schemaFor`](MANAGEMENT.NODE),
machineSchema: this.$store.getters[`management/schemaFor`](CAPI.MACHINE),
clusterToken: null,
etcdBackups: null,
};
},
watch: {
showNodes(neu) {
if (neu) {
this.$store.dispatch('rancher/findAll', { type: NORMAN.NODE });
}
}
},
computed: {
defaultTab() {
if (this.showRegistration && !this.machines?.length) {
return 'registration';
}
if (this.showMachines) {
return 'machine-pools';
}
if (this.showNodes) {
return 'node-pools';
}
return '';
},
fakeMachines() {
// When a deployment has no machines it's not shown.... so add a fake machine to it
// This is a catch all scenario seen in older node pool world but not deployments
const emptyDeployments = this.allMachineDeployments.filter(x => x.spec.clusterName === this.value.metadata.name && x.spec.replicas === 0);
return emptyDeployments.map(d => ({
poolId: d.id,
mainRowKey: 'isFake',
pool: d,
}));
},
machines() {
const machines = this.allMachines.filter((x) => {
if ( x.metadata?.namespace !== this.value.metadata.namespace ) {
return false;
}
return x.spec?.clusterName === this.value.metadata.name;
});
return [...machines, ...this.fakeMachines];
},
nodes() {
const nodes = this.allNodes.filter(x => x.mgmtClusterId === this.value.mgmtClusterId);
return [...nodes, ...this.fakeNodes];
},
fakeNodes() {
// When a pool has no nodes it's not shown.... so add a fake node to it
const emptyNodePools = this.allNodePools.filter(x => x.spec.clusterName === this.value.mgmtClusterId && x.spec.quantity === 0);
return emptyNodePools.map(np => ({
spec: { nodePoolName: np.id.replace('/', ':') },
mainRowKey: 'isFake',
pool: np,
}));
},
showMachines() {
return this.haveMachines && (this.value.isRke2 || !!this.machines.length);
},
showNodes() {
return !this.showMachines && this.haveNodes && !!this.nodes.length;
},
showSnapshots() {
return this.value.isRke2 || this.value.isRke1;
},
machineHeaders() {
return [
STATE,
NAME_COL,
{
name: 'node-name',
labelKey: 'tableHeaders.machineNodeName',
sort: 'status.nodeRef.name',
value: 'status.nodeRef.name',
formatter: 'LinkDetail',
formatterOpts: { reference: 'kubeNodeDetailLocation' }
},
ROLES,
AGE,
];
},
mgmtNodeSchemaHeaders() {
return [
STATE, NAME_COL,
{
name: 'node-name',
labelKey: 'tableHeaders.machineNodeName',
sort: 'kubeNodeName',
value: 'kubeNodeName',
formatter: 'LinkDetail',
formatterOpts: { reference: 'kubeNodeDetailLocation' }
},
ROLES,
AGE
];
},
rke1Snapshots() {
const mgmtId = this.value.mgmt?.id;
if ( !mgmtId ) {
return [];
}
return (this.etcdBackups || []).filter(x => x.clusterId === mgmtId);
},
rke2Snapshots() {
return this.value.etcdSnapshots;
},
rke1SnapshotHeaders() {
return [
STATE_NORMAN,
{
name: 'name',
labelKey: 'tableHeaders.name',
value: 'nameDisplay',
sort: ['nameSort'],
canBeVariable: true,
},
{
name: 'version',
labelKey: 'tableHeaders.version',
value: 'status.kubernetesVersion',
sort: 'status.kubernetesVersion',
width: 150,
},
{ ...AGE_NORMAN, canBeVariable: true },
{
name: 'manual',
labelKey: 'tableHeaders.manual',
value: 'manual',
formatter: 'Checked',
sort: ['manual'],
align: 'center',
width: 50,
},
];
},
rke2SnapshotHeaders() {
return [
STATE_NORMAN,
{
name: 'name',
labelKey: 'tableHeaders.name',
value: 'nameDisplay',
sort: ['nameSort'],
canBeVariable: true,
},
{
name: 'size',
labelKey: 'tableHeaders.size',
value: 'size',
sort: 'size',
formatter: 'Si',
width: 150,
},
{
...AGE,
value: 'createdAt',
sort: 'createdAt:desc',
canBeVariable: true
},
];
},
showRegistration() {
if ( !this.clusterToken ) {
return false;
}
if ( this.value.isImported ) {
return !this.value.mgmt?.isReady;
}
if ( this.value.isCustom ) {
return true;
}
return false;
},
isClusterReady() {
return this.value.mgmt?.isReady;
}
},
mounted() {
window.c = this;
},
methods: {
async takeSnapshot(btnCb) {
try {
await this.value.takeSnapshot();
// Give the change event some time to show up
setTimeout(() => {
btnCb(true);
}, 1000);
} catch (err) {
this.$store.dispatch('growl/fromError', { title: 'Error creating snapshot', err });
btnCb(false);
}
},
showPoolAction(event, pool) {
this.$store.commit(`action-menu/show`, {
resources: [pool],
elem: event.target
});
},
showPoolActionButton(pool) {
return !!pool.availableActions?.length;
},
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<Banner v-if="$fetchState.error" color="error" :label="$fetchState.error" />
<ResourceTabs v-model="value" :default-tab="defaultTab">
<Tab v-if="showMachines" name="machine-pools" :label-key="value.isCustom ? 'cluster.tabs.machines' : 'cluster.tabs.machinePools'" :weight="3">
<ResourceTable
:rows="machines"
:schema="machineSchema"
:headers="machineHeaders"
default-sort-by="name"
:groupable="false"
:group-by="value.isCustom ? null : 'poolId'"
group-ref="pool"
:group-sort="['pool.nameDisplay']"
>
<template #main-row:isFake="{fullColspan}">
<tr class="main-row">
<td :colspan="fullColspan" class="no-entries">
{{ t('node.list.noNodes') }}
</td>
</tr>
</template>
<template #group-by="{group}">
<div class="pool-row" :class="{'has-description':group.ref && group.ref.template}">
<div v-trim-whitespace class="group-tab">
<div v-if="group && group.ref" v-html="group.ref.groupByPoolShortLabel" />
<div v-else v-html="t('resourceTable.groupLabel.notInANodePool')">
</div>
<div v-if="group.ref && group.ref.template" class="description text-muted text-small">
{{ group.ref.providerDisplay }} &ndash; {{ group.ref.providerLocation }} / {{ group.ref.providerSize }} ({{ group.ref.providerName }})
</div>
</div>
<div v-if="group.ref" class="right mr-45">
<template v-if="value.hasLink('update')">
<button v-tooltip="t('node.list.scaleDown')" :disabled="group.ref.spec.replicas < 2" type="button" class="btn btn-sm role-secondary" @click="group.ref.scalePool(-1)">
<i class="icon icon-sm icon-minus" />
</button>
<button v-tooltip="t('node.list.scaleUp')" type="button" class="btn btn-sm role-secondary ml-10" @click="group.ref.scalePool(1)">
<i class="icon icon-sm icon-plus" />
</button>
</template>
</div>
</div>
</template>
</ResourceTable>
</Tab>
<Tab v-else-if="showNodes" name="node-pools" :label-key="value.isCustom ? 'cluster.tabs.machines' : 'cluster.tabs.machinePools'" :weight="3">
<ResourceTable
:schema="mgmtNodeSchema"
:headers="mgmtNodeSchemaHeaders"
:rows="nodes"
:groupable="false"
:group-by="value.isCustom ? null : 'spec.nodePoolName'"
group-ref="pool"
:group-sort="['pool.nameDisplay']"
>
<template #main-row:isFake="{fullColspan}">
<tr class="main-row">
<td :colspan="fullColspan" class="no-entries">
{{ t('node.list.noNodes') }}
</td>
</tr>
</template>
<template #group-by="{group}">
<div class="pool-row" :class="{'has-description':group.ref && group.ref.nodeTemplate}">
<div v-trim-whitespace class="group-tab">
<div v-if="group.ref" v-html="t('resourceTable.groupLabel.nodePool', { name: group.ref.spec.hostnamePrefix, count: group.rows.length}, true)">
</div>
<div v-else v-html="t('resourceTable.groupLabel.notInANodePool')">
</div>
<div v-if="group.ref && group.ref.nodeTemplate" class="description text-muted text-small">
{{ group.ref.providerDisplay }} &ndash; {{ group.ref.providerLocation }} / {{ group.ref.providerSize }} ({{ group.ref.providerName }})
</div>
</div>
<div v-if="group.ref" class="right">
<template v-if="group.ref.hasLink('update')">
<button v-tooltip="t('node.list.scaleDown')" :disabled="group.ref.spec.quantity < 2" type="button" class="btn btn-sm role-secondary" @click="group.ref.scalePool(-1)">
<i class="icon icon-sm icon-minus" />
</button>
<button v-tooltip="t('node.list.scaleUp')" type="button" class="btn btn-sm role-secondary ml-10" @click="group.ref.scalePool(1)">
<i class="icon icon-sm icon-plus" />
</button>
</template>
<button type="button" class="project-action btn btn-sm role-multi-action actions mr-5 ml-15" :class="{invisible: !showPoolActionButton(group.ref)}" @click="showPoolAction($event, group.ref)">
<i class="icon icon-actions" />
</button>
</div>
</div>
</template>
</ResourceTable>
</Tab>
<Tab v-if="showRegistration" name="registration" label="Registration" :weight="2">
<CustomCommand v-if="value.isCustom" :cluster-token="clusterToken" :cluster="value" />
<template v-else>
<h4 v-html="t('cluster.import.commandInstructions', null, true)" />
<CopyCode class="m-10 p-10">
{{ clusterToken.command }}
</CopyCode>
<h4 class="mt-10" v-html="t('cluster.import.commandInstructionsInsecure', null, true)" />
<CopyCode class="m-10 p-10">
{{ clusterToken.insecureCommand }}
</CopyCode>
<h4 class="mt-10" v-html="t('cluster.import.clusterRoleBindingInstructions', null, true)" />
<CopyCode class="m-10 p-10">
{{ t('cluster.import.clusterRoleBindingCommand', null, true) }}
</CopyCode>
</template>
</Tab>
<Tab v-if="showSnapshots" name="snapshots" label="Snapshots" :weight="1">
<SortableTable
:headers="value.isRke1 ? rke1SnapshotHeaders : rke2SnapshotHeaders"
default-sort-by="age"
:table-actions="value.isRke1"
:rows="value.isRke1 ? rke1Snapshots : rke2Snapshots"
:search="false"
>
<template #header-right>
<AsyncButton
mode="snapshot"
class="btn role-primary"
:disabled="!isClusterReady"
@click="takeSnapshot"
/>
</template>
</SortableTable>
</Tab>
</ResourceTabs>
</div>
</template>
<style lang='scss' scoped>
.main-row .no-entries {
text-align: center;
}
.pool-row {
display: flex;
align-items: center;
justify-content: space-between;
&.has-description {
.group-tab {
&, &::after {
height: 50px;
}
&::after {
right: -20px;
}
.description {
margin-top: -20px;
}
}
}
}
</style>