mirror of https://github.com/rancher/dashboard.git
Merge pull request #3348 from WuJun2016/harvester-node-driver
feat: harvester node driver for rke2
This commit is contained in:
commit
3972ac96ef
|
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 194.25619 159.08912"><defs><style>.cls-1{fill:#00a383;}.cls-2{fill:#fff;}</style></defs><rect class="cls-1" width="194.25619" height="159.08912" rx="20.56068"/><path class="cls-2" d="M180.4487,81.46673a4.97417,4.97417,0,0,0,.25287-.81463c.01635-.07209.0296-.14435.04284-.21759a4.98972,4.98972,0,0,0,0-1.78c-.01324-.07324-.02649-.14551-.04284-.21759a4.97458,4.97458,0,0,0-.25287-.81464c-.01587-.03815-.03748-.07269-.05432-.11035a4.9932,4.9932,0,0,0-.23017-.46741l-19.7666-34.2373a4.96949,4.96949,0,0,0-.28741-.42926c-.02527-.03491-.04535-.07251-.07154-.10681a4.97881,4.97881,0,0,0-.57611-.62305c-.05584-.05176-.11346-.10089-.17206-.15033a5.00763,5.00763,0,0,0-.71991-.51843l-.00305-.0022-.00274-.00122a5.00234,5.00234,0,0,0-.80945-.36469c-.07269-.02624-.14459-.05175-.2179-.0744a4.97937,4.97937,0,0,0-.82483-.18683c-.04693-.0061-.0935-.00457-.14044-.00927a4.98006,4.98006,0,0,0-.50464-.03351H116.53335a4.96842,4.96842,0,0,0-.504.03345c-.0476.00482-.09478.00329-.14233.00946a4.97566,4.97566,0,0,0-.82226.18627c-.07447.023-.14753.049-.22138.07563a5.00058,5.00058,0,0,0-.80719.36383l-.00281.00128-.00305.0022a5.00874,5.00874,0,0,0-.71991.51843c-.05859.04944-.11615.09845-.172.15027a4.97536,4.97536,0,0,0-.57635.62335c-.026.034-.04584.07123-.07086.10584a4.98409,4.98409,0,0,0-.2879.43L93.88046,74.54449H79.278l13.99317-24.2373h6.4956a5,5,0,0,0,0-10H90.38443a5.0003,5.0003,0,0,0-4.33008,2.5L67.73111,74.54449h-14.602l13.99316-24.2373h6.49561a5,5,0,0,0,0-10H64.2355a5.00031,5.00031,0,0,0-4.33008,2.5L41.58218,74.54449H26.9797l13.99359-24.2373H47.4689a5,5,0,0,0,0-10H38.08658a5.00029,5.00029,0,0,0-4.33008,2.5L13.98941,77.04449a4.98206,4.98206,0,0,0-.22925.46594c-.01715.0384-.03912.07349-.05536.11243a4.974,4.974,0,0,0-.25183.81128c-.01691.07422-.03058.1485-.04413.22388a4.99817,4.99817,0,0,0-.089.88305l-.00036.00342.00036.00342a4.99828,4.99828,0,0,0,.089.88306c.01355.07538.02722.14966.04413.22387a4.97371,4.97371,0,0,0,.25183.81128c.01624.03894.03821.074.05536.11243a4.98084,4.98084,0,0,0,.22925.46594L33.7565,116.28131a5.00031,5.00031,0,0,0,4.33008,2.5H47.4689a5,5,0,1,0,0-10H40.97329L26.97976,84.54449H41.58218l18.32324,31.73682a5.00033,5.00033,0,0,0,4.33008,2.5h9.38233a5,5,0,0,0,0-10H67.12222L53.12912,84.54449h14.602l18.32324,31.73682a5.00032,5.00032,0,0,0,4.33008,2.5h9.38232a5,5,0,0,0,0-10h-6.4956L79.278,84.54449H93.88052l18.32276,31.73682a4.97193,4.97193,0,0,0,.28759.42944c.02521.03479.04517.07233.07135.10657a4.98039,4.98039,0,0,0,.57605.62311c.05591.05188.11371.10095.17237.15051a5.00138,5.00138,0,0,0,.71966.51825l.00305.0022c.02063.0119.04254.01953.0633.03112a4.99593,4.99593,0,0,0,.52386.25788c.15222.06329.30756.11084.46283.15808.051.0155.09961.037.15119.0509a4.96725,4.96725,0,0,0,1.29394.17292l.01508-.001h39.51379l.01508.001a4.96725,4.96725,0,0,0,1.29394-.17292c.05158-.01391.10016-.0354.15119-.0509.15527-.04724.3106-.09479.46282-.15808a4.99511,4.99511,0,0,0,.52387-.25788c.02075-.01159.04266-.01922.06329-.03112l.00312-.0022a5.00713,5.00713,0,0,0,.71856-.51733c.05939-.05011.11774-.09986.17432-.15229a4.97862,4.97862,0,0,0,.57458-.6214c.027-.03546.04773-.07415.0738-.11023a4.98327,4.98327,0,0,0,.2857-.42663l19.7666-34.23682a4.99182,4.99182,0,0,0,.23017-.4674C180.41122,81.53943,180.43283,81.50488,180.4487,81.46673Zm-24.38111,22.31452L150.2941,93.78094l5.33294-9.23645h11.54688ZM105.42776,84.54449h11.54706l5.33289,9.23633-5.77417,10.00061ZM116.5336,55.307l5.77411,10.00031-5.33295,9.23718H105.4277Zm14.43432,33.47382-5.333-9.23633,5.333-9.2373h10.666l5.333,9.2373-5.333,9.23633Zm10.666-28.47363H130.9678l-5.77392-10h22.2135ZM130.96786,98.78082h10.666l5.77362,10.00049H125.19375ZM155.6271,74.54449l-5.333-9.2373,5.77343-10L167.174,74.54449Z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
|
@ -941,6 +941,25 @@ cluster:
|
|||
password:
|
||||
label: Password
|
||||
note: 'Note: The free ESXi license does not support API access. Only servers with a valid or evaluation license are supported.'
|
||||
harvester:
|
||||
import: Imported Harvester
|
||||
external: External Harvester
|
||||
namespace: Namespace
|
||||
cpu: CPUs
|
||||
memory: Memory
|
||||
disk: Disk
|
||||
image: Image
|
||||
network: Network Name
|
||||
sshUser: SSH User
|
||||
userData:
|
||||
label: User Data Template
|
||||
title: "User Data:"
|
||||
networkData:
|
||||
label: Network Data Template
|
||||
title: "Network Data:"
|
||||
kubeconfigContent:
|
||||
label: KubeconfigContent
|
||||
placeholder: 'Namespace/Name'
|
||||
description:
|
||||
label: Cluster Description
|
||||
placeholder: Any text you want that better describes this cluster
|
||||
|
|
|
|||
|
|
@ -784,6 +784,26 @@ cluster:
|
|||
#label: Access Token
|
||||
placeholder: 请输入您的 DigitalOcean API Access Token
|
||||
help: 从 DigitalOcean <a href="https://cloud.digitalocean.com/settings/api/tokens" target="_blank" rel="noopener noreferrer nofollow">Applications & API</a>中复制和粘贴个人访问令牌。
|
||||
harvester:
|
||||
import: 导入的 Harvester
|
||||
external: 外部的 Harvester
|
||||
namespace: 命名空间
|
||||
cpu: CPUs
|
||||
memory: 内存
|
||||
disk: 磁盘
|
||||
image: 镜像
|
||||
network: 网络
|
||||
sshUser: SSH 用户名
|
||||
userData:
|
||||
label: 用户配置模板
|
||||
title: "用户数据:"
|
||||
networkData:
|
||||
label: 网络配置模板
|
||||
title: "网络数据:"
|
||||
kubeconfigContent:
|
||||
label: Kubeconfig 文件
|
||||
cluster: 集群
|
||||
placeholder: 命名空间/名称
|
||||
name:
|
||||
label: 集群名称
|
||||
placeholder: 请输入集群名称,该名称不能与其他集群名称相同
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
<script>
|
||||
import CreateEditView from '@/mixins/create-edit-view';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import RadioGroup from '@/components/form/RadioGroup';
|
||||
|
||||
import { get } from '@/utils/object';
|
||||
import { MANAGEMENT } from '@/config/types';
|
||||
import { HCI } from '@/config/labels-annotations';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LabeledInput, LabeledSelect, RadioGroup
|
||||
},
|
||||
mixins: [CreateEditView],
|
||||
|
||||
async fetch() {
|
||||
this.clusters = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER });
|
||||
},
|
||||
|
||||
data() {
|
||||
this.$emit('validationChanged', true);
|
||||
|
||||
if (!this.value.decodedData.clusterType) {
|
||||
this.value.setData('clusterType', 'import');
|
||||
}
|
||||
|
||||
const cluster = get(this.value, `metadata.annotations."${ HCI.CLUSTER_ID }"`) || '';
|
||||
|
||||
return {
|
||||
clusters: [],
|
||||
cluster,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
clusterOptions() { // TODO: Filter out all harvester clusters
|
||||
return this.clusters.map( (cluster) => {
|
||||
return {
|
||||
value: cluster.id,
|
||||
label: cluster.nameDisplay
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
isImportCluster() {
|
||||
return this.value.decodedData.clusterType === 'import';
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'value.decodedData.clusterType': {
|
||||
handler(neu) {
|
||||
if (this.isCreate) {
|
||||
this.value.setData('kubeconfigContent', '');
|
||||
this.cluster = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
async cluster(neu) {
|
||||
if (!neu) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isCreate) {
|
||||
this.value.setAnnotation(HCI.CLUSTER_ID, neu);
|
||||
}
|
||||
|
||||
const currentCluster = this.$store.getters['management/all'](MANAGEMENT.CLUSTER).find(x => x.id === neu);
|
||||
|
||||
this.$nuxt.$loading.start();
|
||||
|
||||
const kubeconfigContent = await currentCluster.generateKubeConfig();
|
||||
|
||||
this.$nuxt.$loading.finish();
|
||||
|
||||
this.value.setData('kubeconfigContent', kubeconfigContent);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
test() {
|
||||
const t = this.$store.getters['i18n/t'];
|
||||
|
||||
if (!this.cluster && this.isImportCluster) {
|
||||
const cluster = t('cluster.credential.harvester.cluster');
|
||||
const errors = [t('validation.required', { key: cluster })];
|
||||
|
||||
return { errors };
|
||||
}
|
||||
|
||||
if (!this.value.decodedData.kubeconfigContent) {
|
||||
const kubeconfigContent = t('cluster.credential.harvester.kubeconfigContent.label');
|
||||
|
||||
const errors = [t('validation.required', { key: kubeconfigContent })];
|
||||
|
||||
return { errors };
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row mb-10">
|
||||
<RadioGroup
|
||||
v-model="value.decodedData.clusterType"
|
||||
:mode="mode"
|
||||
:disabled="isEdit"
|
||||
name="clusterType"
|
||||
:labels="[t('cluster.credential.harvester.import'),t('cluster.credential.harvester.external')]"
|
||||
:options="['import', 'external']"
|
||||
@input="value.setData('clusterType', $event);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row mb-10">
|
||||
<div v-if="isImportCluster" class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model="cluster"
|
||||
:mode="mode"
|
||||
:disabled="isEdit"
|
||||
:options="clusterOptions"
|
||||
:required="true"
|
||||
label="Cluster"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-if="!isImportCluster"
|
||||
:value="value.decodedData.kubeconfigContent"
|
||||
label-key="cluster.credential.harvester.kubeconfigContent.label"
|
||||
:required="true"
|
||||
type="multiline"
|
||||
:min-height="160"
|
||||
:mode="mode"
|
||||
@input="value.setData('kubeconfigContent', $event);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -189,7 +189,9 @@ export default {
|
|||
},
|
||||
|
||||
tabAddClicked() {
|
||||
this.$emit('addTab');
|
||||
const activeTabIndex = findIndex(this.tabs, tab => tab.active);
|
||||
|
||||
this.$emit('addTab', activeTabIndex);
|
||||
},
|
||||
|
||||
tabRemoveClicked() {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export default {
|
|||
userValue() {
|
||||
let userValue = '';
|
||||
|
||||
if ( this.value !== null && this.value !== undefined ) {
|
||||
if ( this.value !== null && this.value !== undefined && this.value !== '' && this.value !== 'null') {
|
||||
userValue = parseSi(`${ this.value } ${ this.unit || '' }`, {
|
||||
addSuffix: false,
|
||||
increment: this.increment,
|
||||
|
|
@ -114,7 +114,7 @@ export default {
|
|||
}
|
||||
|
||||
if ( this.outputAs === 'string' ) {
|
||||
out = `${ out }`;
|
||||
out = out === null ? '' : `${ out }`;
|
||||
}
|
||||
|
||||
this.$emit('input', out);
|
||||
|
|
|
|||
|
|
@ -109,3 +109,8 @@ export const ANNOTATIONS_TO_FOLD = [
|
|||
/^kubectl\.kubernetes\.io\/.*$/,
|
||||
/^objectset\.rio\.cattle\.io\/.*$/,
|
||||
];
|
||||
|
||||
export const HCI = {
|
||||
CLUSTER_ID: 'harvesterhci.io/clusterId',
|
||||
CLOUD_INIT: 'harvesterhci.io/cloud-init-template'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -242,3 +242,25 @@ export const VIRTUAL_TYPES = {
|
|||
PROJECT_NAMESPACES: 'projects-namespaces',
|
||||
NAMESPACES: 'namespaces'
|
||||
};
|
||||
|
||||
// harvester
|
||||
export const HCI = {
|
||||
VM: 'kubevirt.io.virtualmachine',
|
||||
VMI: 'kubevirt.io.virtualmachineinstance',
|
||||
VMIM: 'kubevirt.io.virtualmachineinstancemigration',
|
||||
VM_TEMPLATE: 'harvesterhci.io.virtualmachinetemplate',
|
||||
VM_VERSION: 'harvesterhci.io.virtualmachinetemplateversion',
|
||||
IMAGE: 'harvesterhci.io.virtualmachineimage',
|
||||
SSH: 'harvesterhci.io.keypair',
|
||||
DATA_VOLUME: 'cdi.kubevirt.io.datavolume',
|
||||
USER: 'harvesterhci.io.user',
|
||||
SETTING: 'harvesterhci.io.setting',
|
||||
UPGRADE: 'harvesterhci.io.upgrade',
|
||||
BACKUP: 'harvesterhci.io.virtualmachinebackup',
|
||||
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
||||
BACKUP_CONTENT: 'harvesterhci.io.virtualmachinebackupcontent',
|
||||
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
||||
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
|
||||
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
|
||||
NETWORK_ATTACHMENT: 'k8s.cni.cncf.io.networkattachmentdefinition',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -56,6 +56,28 @@ export default {
|
|||
return importMachineConfig('generic');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async test() {
|
||||
if ( typeof this.$refs.configComponent?.test === 'function' ) {
|
||||
let errors = [];
|
||||
|
||||
try {
|
||||
const res = await this.$refs.configComponent.test();
|
||||
|
||||
if ( !res || res?.errors) {
|
||||
if (res?.errors) {
|
||||
errors = res.errors;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
errors = [e];
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -87,6 +109,7 @@ export default {
|
|||
|
||||
<component
|
||||
:is="configComponent"
|
||||
ref="configComponent"
|
||||
:uuid="uuid"
|
||||
:mode="mode"
|
||||
:value="value.config"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { CAPI, MANAGEMENT, SECRET } from '@/config/types';
|
|||
import { _CREATE, _EDIT } from '@/config/query-params';
|
||||
import { DEFAULT_WORKSPACE } from '@/models/provisioning.cattle.io.cluster';
|
||||
|
||||
import { findBy, removeObject } from '@/utils/array';
|
||||
import { findBy, removeObject, clear } from '@/utils/array';
|
||||
import { clone, diff, isEmpty, set } from '@/utils/object';
|
||||
import { allHash } from '@/utils/promise';
|
||||
import { sortBy } from '@/utils/sort';
|
||||
|
|
@ -605,7 +605,7 @@ export default {
|
|||
this.machinePools = out;
|
||||
},
|
||||
|
||||
async addMachinePool() {
|
||||
async addMachinePool(idx) {
|
||||
if ( !this.machineConfigSchema ) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -617,7 +617,7 @@ export default {
|
|||
metadata: { namespace: DEFAULT_WORKSPACE }
|
||||
});
|
||||
|
||||
config.applyDefaults();
|
||||
config.applyDefaults(idx, this.machinePools);
|
||||
|
||||
const name = `pool${ ++this.lastIdx }`;
|
||||
|
||||
|
|
@ -734,8 +734,30 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
saveOverride() {
|
||||
this.save(...arguments);
|
||||
async saveOverride(btnCb) {
|
||||
if ( this.errors ) {
|
||||
clear(this.errors);
|
||||
}
|
||||
|
||||
for (const [index] of this.machinePools.entries()) { // validator machine config
|
||||
if ( typeof this.$refs.pool[index]?.test === 'function' ) {
|
||||
try {
|
||||
const res = await this.$refs.pool[index].test();
|
||||
|
||||
if (Array.isArray(res) && res.length > 0) {
|
||||
this.errors.push(...res);
|
||||
}
|
||||
} catch (e) {
|
||||
this.errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.errors.length) {
|
||||
btnCb(false);
|
||||
} else {
|
||||
return this.save(btnCb);
|
||||
}
|
||||
|
||||
this.value.waitForMgmt().then(() => {
|
||||
if (this.membershipUpdate.save) {
|
||||
|
|
@ -890,11 +912,12 @@ export default {
|
|||
ref="pools"
|
||||
:side-tabs="true"
|
||||
:show-tabs-add-remove="!isView"
|
||||
@addTab="addMachinePool"
|
||||
@addTab="addMachinePool($event)"
|
||||
@removeTab="removeMachinePool($event)"
|
||||
>
|
||||
<Tab v-for="obj in machinePools" :key="obj.id" :name="obj.id" :label="obj.pool.name || '(Not Named)'" :show-header="false">
|
||||
<MachinePool
|
||||
ref="pool"
|
||||
:value="obj"
|
||||
:mode="mode"
|
||||
:provider="provider"
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ export default {
|
|||
|
||||
if ( newCloudCred ) {
|
||||
this.value.metadata.namespace = DEFAULT_WORKSPACE;
|
||||
|
||||
this.$set(this.value.metadata, 'name', '');
|
||||
|
||||
this.$set(this.value, 'data', {});
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -192,8 +196,12 @@ export default {
|
|||
try {
|
||||
const res = await this.$refs.cloudComponent.test();
|
||||
|
||||
if ( !res ) {
|
||||
this.errors = ['Authentication test failed, please check your credentials'];
|
||||
if ( !res || res?.errors) {
|
||||
if (res?.errors) {
|
||||
this.errors = res.errors;
|
||||
} else {
|
||||
this.errors = ['Authentication test failed, please check your credentials'];
|
||||
}
|
||||
btnCb(false);
|
||||
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,463 @@
|
|||
<script>
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import Loading from '@/components/Loading';
|
||||
import CreateEditView from '@/mixins/create-edit-view';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import UnitInput from '@/components/form/UnitInput';
|
||||
import YamlEditor from '@/components/YamlEditor';
|
||||
import Banner from '@/components/Banner';
|
||||
|
||||
import { get } from '@/utils/object';
|
||||
import { mapGetters } from 'vuex';
|
||||
import {
|
||||
SECRET, HCI, NAMESPACE, MANAGEMENT, CONFIG_MAP
|
||||
} from '@/config/types';
|
||||
import { base64Decode, base64Encode } from '@/utils/crypto';
|
||||
import { allHashSettled } from '@/utils/promise';
|
||||
import { stringify, exceptionToErrorsArray } from '@/utils/error';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@/config/labels-annotations';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Loading, LabeledSelect, LabeledInput, UnitInput, Banner, YamlEditor
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
credentialId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
uuid: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
this.errors = [];
|
||||
|
||||
try {
|
||||
this.credential = await this.$store.dispatch('management/find', { type: SECRET, id: this.credentialId });
|
||||
const clusterId = get(this.credential, `metadata.annotations."${ HCI_ANNOTATIONS.CLUSTER_ID }"`);
|
||||
|
||||
const url = `/k8s/clusters/${ clusterId }/v1`;
|
||||
const isImportCluster = this.credential.decodedData.clusterType === 'import';
|
||||
|
||||
this.isImportCluster = isImportCluster;
|
||||
|
||||
if (clusterId && isImportCluster) {
|
||||
const res = await allHashSettled({
|
||||
namespaces: this.$store.dispatch('cluster/request', { url: `${ url }/${ NAMESPACE }s` }),
|
||||
images: this.$store.dispatch('cluster/request', { url: `${ url }/${ HCI.IMAGE }s` }),
|
||||
configMaps: this.$store.dispatch('cluster/request', { url: `${ url }/${ CONFIG_MAP }s` }),
|
||||
networks: this.$store.dispatch('cluster/request', { url: `${ url }/k8s.cni.cncf.io.network-attachment-definitions` }),
|
||||
});
|
||||
|
||||
for ( const key of Object.keys(res) ) {
|
||||
const obj = res[key];
|
||||
|
||||
if ( obj.status === 'rejected' ) {
|
||||
this.errors.push(stringify(obj.reason));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.errors.length > 0) { // If an error is reported in the request data, see if it is due to a cluster error
|
||||
const cluster = await this.$store.dispatch('management/find', { type: MANAGEMENT.CLUSTER, id: clusterId });
|
||||
|
||||
if (cluster.stateDescription && !cluster.isReady) {
|
||||
this.errors = [cluster.stateDescription];
|
||||
}
|
||||
}
|
||||
|
||||
const userDataOptions = [];
|
||||
const networkDataOptions = [];
|
||||
|
||||
(res.configMaps.value?.data || []).map((O) => {
|
||||
const cloudTemplate = O.metadata?.labels?.[HCI_ANNOTATIONS.CLOUD_INIT];
|
||||
|
||||
if (cloudTemplate === 'user') {
|
||||
userDataOptions.push({
|
||||
label: O.metadata.name,
|
||||
value: O.data.cloudInit
|
||||
});
|
||||
}
|
||||
|
||||
if (cloudTemplate === 'network') {
|
||||
networkDataOptions.push({
|
||||
label: O.metadata.name,
|
||||
value: O.data.cloudInit
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.userDataOptions = userDataOptions;
|
||||
this.networkDataOptions = networkDataOptions;
|
||||
|
||||
this.imageOptions = (res.images.value?.data || []).filter( (O) => {
|
||||
return !O.spec.url.endsWith('.iso');
|
||||
}).map( (O) => {
|
||||
const value = O.id;
|
||||
const label = `${ O.spec.displayName } (${ value })`;
|
||||
|
||||
return {
|
||||
label,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
this.networkOptions = (res.networks.value?.data || []).map( (O) => {
|
||||
let value;
|
||||
let label;
|
||||
|
||||
try {
|
||||
const config = JSON.parse(O.spec.config);
|
||||
|
||||
const id = config.vlan;
|
||||
|
||||
value = O.id;
|
||||
label = `${ value } (vlanId=${ id })`;
|
||||
} catch (err) {}
|
||||
|
||||
return {
|
||||
label,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
(res.namespaces.value?.data || []).forEach(async(namespace) => {
|
||||
const proxyNamespace = await this.$store.dispatch('cluster/create', namespace);
|
||||
|
||||
if (!proxyNamespace.isSystem) {
|
||||
const value = namespace.metadata.name;
|
||||
const label = namespace.metadata.name;
|
||||
|
||||
this.namespaceOptions.push({
|
||||
label,
|
||||
value
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isEmpty(this.value.cpuCount)) {
|
||||
this.value.cpuCount = '2';
|
||||
}
|
||||
|
||||
if (isEmpty(this.value.memorySize)) {
|
||||
this.value.memorySize = '4';
|
||||
}
|
||||
|
||||
if (isEmpty(this.value.diskSize)) {
|
||||
this.value.diskSize = '40';
|
||||
}
|
||||
} catch (e) {
|
||||
this.errors = exceptionToErrorsArray(e);
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
let userData = '';
|
||||
let networkData = '';
|
||||
|
||||
if (this.value.userData) {
|
||||
userData = base64Decode(this.value.userData);
|
||||
}
|
||||
|
||||
if (this.value.networkData) {
|
||||
networkData = base64Decode(this.value.networkData);
|
||||
}
|
||||
|
||||
return {
|
||||
credential: null,
|
||||
isImportCluster: false,
|
||||
userData,
|
||||
networkData,
|
||||
imageOptions: [],
|
||||
namespaceOptions: [],
|
||||
networkOptions: [],
|
||||
userDataOptions: [],
|
||||
networkDataOptions: [],
|
||||
cpuCount: ''
|
||||
};
|
||||
},
|
||||
|
||||
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||
|
||||
watch: {
|
||||
'credentialId'() {
|
||||
this.imageOptions = [];
|
||||
this.networkOptions = [];
|
||||
this.namespaceOptions = [];
|
||||
this.value.imageName = '';
|
||||
this.value.networkName = '';
|
||||
this.value.vmNamespace = '';
|
||||
|
||||
this.$fetch();
|
||||
},
|
||||
|
||||
networkData(neu) {
|
||||
this.$refs.networkYamlEditor.refresh();
|
||||
this.value.networkData = base64Encode(neu);
|
||||
},
|
||||
|
||||
userData(neu) {
|
||||
this.$refs.userDataYamlEditor.refresh();
|
||||
this.value.userData = base64Encode(neu);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
stringify,
|
||||
|
||||
test() {
|
||||
const errors = [];
|
||||
|
||||
if (!this.value.cpuCount) {
|
||||
const message = this.validatorRequiredField(this.t('cluster.credential.harvester.cpu'));
|
||||
|
||||
errors.push(message);
|
||||
}
|
||||
|
||||
if (!this.value.vmNamespace) {
|
||||
const message = this.validatorRequiredField(this.t('cluster.credential.harvester.namespace'));
|
||||
|
||||
errors.push(message);
|
||||
}
|
||||
|
||||
if (!this.value.memorySize) {
|
||||
const message = this.validatorRequiredField(this.t('cluster.credential.harvester.memory'));
|
||||
|
||||
errors.push(message);
|
||||
}
|
||||
|
||||
if (!this.value.diskSize) {
|
||||
const message = this.validatorRequiredField(this.t('cluster.credential.harvester.disk'));
|
||||
|
||||
errors.push(message);
|
||||
}
|
||||
|
||||
if (!this.value.imageName) {
|
||||
const message = this.validatorRequiredField(this.t('cluster.credential.harvester.image'));
|
||||
|
||||
errors.push(message);
|
||||
}
|
||||
|
||||
if (!this.value.sshUser) {
|
||||
const message = this.validatorRequiredField(this.t('cluster.credential.harvester.sshUser'));
|
||||
|
||||
errors.push(message);
|
||||
}
|
||||
|
||||
if (!this.value.networkName) {
|
||||
const message = this.validatorRequiredField(this.t('cluster.credential.harvester.network'));
|
||||
|
||||
errors.push(message);
|
||||
}
|
||||
|
||||
return { errors };
|
||||
},
|
||||
|
||||
validatorRequiredField(key) {
|
||||
return this.t('validation.required', { key });
|
||||
},
|
||||
|
||||
valuesChanged(value, type) {
|
||||
this.value[type] = base64Encode(value);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" :delayed="true" />
|
||||
<div v-else-if="errors.length">
|
||||
<div
|
||||
v-for="(err, idx) in errors"
|
||||
:key="idx"
|
||||
>
|
||||
<Banner
|
||||
color="error"
|
||||
:label="stringify(err)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<UnitInput
|
||||
v-model="value.cpuCount"
|
||||
v-int-number
|
||||
label-key="cluster.credential.harvester.cpu"
|
||||
suffix="C"
|
||||
output-as="string"
|
||||
required
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<UnitInput
|
||||
v-model="value.memorySize"
|
||||
v-int-number
|
||||
label-key="cluster.credential.harvester.memory"
|
||||
output-as="string"
|
||||
suffix="GiB"
|
||||
:mode="mode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<UnitInput
|
||||
v-model="value.diskSize"
|
||||
v-int-number
|
||||
label-key="cluster.credential.harvester.disk"
|
||||
output-as="string"
|
||||
suffix="GiB"
|
||||
:mode="mode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-if="isImportCluster"
|
||||
v-model="value.vmNamespace"
|
||||
:mode="mode"
|
||||
:options="namespaceOptions"
|
||||
:searchable="true"
|
||||
:required="true"
|
||||
label-key="cluster.credential.harvester.namespace"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-else
|
||||
v-model="value.vmNamespace"
|
||||
label-key="cluster.credential.harvester.namespace"
|
||||
:required="true"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isImportCluster" class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model="value.imageName"
|
||||
:mode="mode"
|
||||
:options="imageOptions"
|
||||
:required="true"
|
||||
label-key="cluster.credential.harvester.image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model="value.networkName"
|
||||
:mode="mode"
|
||||
:options="networkOptions"
|
||||
:required="true"
|
||||
label-key="cluster.credential.harvester.network"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="value.imageName"
|
||||
:mode="mode"
|
||||
:required="true"
|
||||
:placeholder="t('cluster.credential.harvester.placeholder')"
|
||||
label-key="cluster.credential.harvester.image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="value.networkName"
|
||||
:mode="mode"
|
||||
:required="true"
|
||||
:placeholder="t('cluster.credential.harvester.placeholder')"
|
||||
label-key="cluster.credential.harvester.network"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model="value.sshUser"
|
||||
label-key="cluster.credential.harvester.sshUser"
|
||||
:required="true"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<portal :to="'advanced-'+uuid">
|
||||
<h3>{{ t("cluster.credential.harvester.userData.title") }}</h3>
|
||||
<div>
|
||||
<LabeledSelect
|
||||
v-if="isImportCluster && isCreate"
|
||||
v-model="userData"
|
||||
class="mb-10"
|
||||
:options="userDataOptions"
|
||||
label-key="cluster.credential.harvester.userData.label"
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
<YamlEditor
|
||||
ref="userDataYamlEditor"
|
||||
:key="userData"
|
||||
class="yaml-editor mb-20"
|
||||
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
||||
:value="userData"
|
||||
@onInput="valuesChanged($event, 'userData')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>{{ t("cluster.credential.harvester.networkData.title") }}</h3>
|
||||
<div>
|
||||
<LabeledSelect
|
||||
v-if="isImportCluster && isCreate"
|
||||
v-model="networkData"
|
||||
class="mb-10"
|
||||
:options="networkDataOptions"
|
||||
label-key="cluster.credential.harvester.networkData.label"
|
||||
:mode="mode"
|
||||
/>
|
||||
|
||||
<YamlEditor
|
||||
ref="networkYamlEditor"
|
||||
:key="networkData"
|
||||
class="yaml-editor mb-10"
|
||||
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
||||
:value="networkData"
|
||||
@onInput="valuesChanged($event, 'networkData')"
|
||||
/>
|
||||
</div>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$yaml-height: 200px;
|
||||
|
||||
::v-deep .yaml-editor{
|
||||
flex: 1;
|
||||
min-height: $yaml-height;
|
||||
& .code-mirror .CodeMirror {
|
||||
position: initial;
|
||||
height: auto;
|
||||
min-height: $yaml-height;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -14,5 +14,5 @@ export default {
|
|||
// }
|
||||
|
||||
return keys.join(', ');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
export default {
|
||||
applyDefaults() {
|
||||
return (idx, machinePools) => {
|
||||
const _machinePools = cloneDeep(machinePools);
|
||||
|
||||
if (_machinePools[idx]) {
|
||||
const copyConfig = _machinePools[idx]?.config;
|
||||
|
||||
merge(this, copyConfig);
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -309,6 +309,7 @@ module.exports = {
|
|||
'~/plugins/trim-whitespace',
|
||||
{ src: '~/plugins/extend-router' },
|
||||
{ src: '~/plugins/lookup', ssr: false },
|
||||
{ src: '~/plugins/int-number', ssr: false },
|
||||
{ src: '~/plugins/nuxt-client-init', ssr: false },
|
||||
'~/plugins/replaceall',
|
||||
'~/plugins/back-button',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export default Vue.directive('intNumber', {
|
||||
inserted(el) {
|
||||
el.addEventListener('keypress', (e) => {
|
||||
e = e || window.event;
|
||||
const charcode = typeof e.charCode === 'number' ? e.charCode : e.keyCode;
|
||||
const re = /\d/;
|
||||
|
||||
if (!re.test(String.fromCharCode(charcode)) && charcode > 9 && !e.ctrlKey) {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
e.returnValue = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue