Merge pull request #3348 from WuJun2016/harvester-node-driver

feat: harvester node driver for rke2
This commit is contained in:
Vincent Fiduccia 2021-07-16 10:22:12 -07:00 committed by GitHub
commit 3972ac96ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 781 additions and 12 deletions

View File

@ -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

View File

@ -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

View File

@ -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: 请输入集群名称,该名称不能与其他集群名称相同

View File

@ -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>

View File

@ -189,7 +189,9 @@ export default {
},
tabAddClicked() {
this.$emit('addTab');
const activeTabIndex = findIndex(this.tabs, tab => tab.active);
this.$emit('addTab', activeTabIndex);
},
tabRemoveClicked() {

View File

@ -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);

View File

@ -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'
};

View File

@ -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',
};

View File

@ -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"

View File

@ -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"

View File

@ -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;

View File

@ -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>

View File

@ -14,5 +14,5 @@ export default {
// }
return keys.join(', ');
}
},
};

View File

@ -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);
}
};
},
};

View File

@ -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',

19
plugins/int-number.js Normal file
View File

@ -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;
}
}
});
}
});