mirror of https://github.com/rancher/dashboard.git
682 lines
19 KiB
Vue
682 lines
19 KiB
Vue
<script>
|
|
import Loading from '@shell/components/Loading';
|
|
import { Banner } from '@components/Banner';
|
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
import KeyValue from '@shell/components/form/KeyValue';
|
|
import UnitInput from '@shell/components/form/UnitInput';
|
|
import { RadioGroup } from '@components/Form/Radio';
|
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
import { NORMAN } from '@shell/config/types';
|
|
import { allHash } from '@shell/utils/promise';
|
|
import { addObject, addObjects, findBy } from '@shell/utils/array';
|
|
import { convertStringToKV, convertKVToString } from '@shell/utils/object';
|
|
import { sortBy } from '@shell/utils/sort';
|
|
import { stringify, exceptionToErrorsArray } from '@shell/utils/error';
|
|
|
|
const DEFAULT_GROUP = 'rancher-nodes';
|
|
|
|
export default {
|
|
components: {
|
|
Banner, Loading, LabeledInput, LabeledSelect, Checkbox, RadioGroup, UnitInput, KeyValue
|
|
},
|
|
|
|
mixins: [CreateEditView],
|
|
|
|
props: {
|
|
uuid: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
|
|
cluster: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
|
|
credentialId: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
},
|
|
|
|
async fetch() {
|
|
this.errors = [];
|
|
if ( !this.credentialId ) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if ( this.credential?.id !== this.credentialId ) {
|
|
this.credential = await this.$store.dispatch('rancher/find', { type: NORMAN.CLOUD_CREDENTIAL, id: this.credentialId });
|
|
}
|
|
} catch (e) {
|
|
this.credential = null;
|
|
}
|
|
|
|
try {
|
|
const region = this.value.region || this.credential?.decodedData.defaultRegion || this.$store.getters['aws/defaultRegion'];
|
|
|
|
if ( !this.value.region ) {
|
|
this.value['region'] = region;
|
|
}
|
|
|
|
this.ec2Client = await this.$store.dispatch('aws/ec2', { region, cloudCredentialId: this.credentialId });
|
|
this.kmsClient = await this.$store.dispatch('aws/kms', { region, cloudCredentialId: this.credentialId });
|
|
|
|
if ( !this.instanceInfo ) {
|
|
this.instanceInfo = await this.$store.dispatch('aws/describeInstanceTypes', { client: this.ec2Client } );
|
|
}
|
|
|
|
const hash = {};
|
|
|
|
if ( !this.regionInfo ) {
|
|
hash.regionInfo = this.ec2Client.describeRegions({});
|
|
}
|
|
|
|
if ( this.loadedRegionalFor !== region ) {
|
|
hash.zoneInfo = await this.ec2Client.describeAvailabilityZones({});
|
|
hash.vpcInfo = await this.ec2Client.describeVpcs({});
|
|
hash.subnetInfo = await this.ec2Client.describeSubnets({});
|
|
hash.securityGroupInfo = await this.ec2Client.describeSecurityGroups({});
|
|
}
|
|
|
|
const res = await allHash(hash);
|
|
|
|
for ( const k in res ) {
|
|
this[k] = res[k];
|
|
}
|
|
|
|
try {
|
|
this.kmsInfo = await this.kmsClient.listKeys({});
|
|
this.canReadKms = true;
|
|
} catch (e) {
|
|
this.canReadKms = false;
|
|
}
|
|
|
|
if ( !this.value.zone ) {
|
|
this.value['zone'] = 'a';
|
|
}
|
|
|
|
if ( !this.value.instanceType ) {
|
|
this.value['instanceType'] = this.$store.getters['aws/defaultInstanceType'];
|
|
}
|
|
|
|
this.initNetwork();
|
|
this.initTags();
|
|
|
|
if ( !this.value.securityGroup?.length ) {
|
|
this.value['securityGroup'] = [DEFAULT_GROUP];
|
|
}
|
|
|
|
if ( this.value.securityGroup?.length === 1 && this.value.securityGroup[0] === DEFAULT_GROUP ) {
|
|
this.securityGroupMode = 'default';
|
|
} else {
|
|
this.securityGroupMode = 'custom';
|
|
}
|
|
|
|
this.loadedRegionalFor = region;
|
|
} catch (e) {
|
|
this.errors = exceptionToErrorsArray(e);
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
ec2Client: null,
|
|
kmsClient: null,
|
|
credential: null,
|
|
instanceInfo: null,
|
|
regionInfo: null,
|
|
canReadKms: null,
|
|
kmsInfo: null,
|
|
tags: null,
|
|
loadedRegionalFor: null,
|
|
zoneInfo: null,
|
|
vpcInfo: null,
|
|
subnetInfo: null,
|
|
securityGroupInfo: null,
|
|
selectedNetwork: null,
|
|
securityGroupMode: null,
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
securityGroupLabels() {
|
|
return [
|
|
this.t('cluster.machineConfig.amazonEc2.securityGroup.mode.default', { defaultGroup: DEFAULT_GROUP }),
|
|
this.t('cluster.machineConfig.amazonEc2.securityGroup.mode.custom')
|
|
];
|
|
},
|
|
|
|
isIamInstanceProfileNameRequired() {
|
|
return this.cluster?.cloudProvider === 'aws';
|
|
},
|
|
|
|
instanceOptions() {
|
|
let lastGroup;
|
|
|
|
const out = [];
|
|
|
|
for ( const row of this.instanceInfo ) {
|
|
if ( row.groupLabel !== lastGroup ) {
|
|
out.push({
|
|
kind: 'group',
|
|
disabled: true,
|
|
label: row.groupLabel
|
|
});
|
|
|
|
lastGroup = row.groupLabel;
|
|
}
|
|
|
|
out.push({
|
|
label: row['label'],
|
|
value: row['apiName'],
|
|
});
|
|
}
|
|
|
|
return out;
|
|
},
|
|
|
|
regionOptions() {
|
|
if ( !this.regionInfo ) {
|
|
return [];
|
|
}
|
|
|
|
return this.regionInfo.Regions.map((obj) => {
|
|
return obj.RegionName;
|
|
}).sort();
|
|
},
|
|
|
|
zoneOptions() {
|
|
if ( !this.zoneInfo ) {
|
|
return [];
|
|
}
|
|
|
|
return this.zoneInfo.AvailabilityZones.map((obj) => {
|
|
return obj.ZoneName.substr(-1);
|
|
}).sort();
|
|
},
|
|
|
|
networkOptions() {
|
|
if ( !this.vpcInfo || !this.subnetInfo ) {
|
|
return [];
|
|
}
|
|
|
|
let vpcs = [];
|
|
const subnetsByVpc = {};
|
|
|
|
for ( const obj of this.vpcInfo.Vpcs ) {
|
|
const name = obj.Tags && obj.Tags?.length ? obj.Tags.find((t) => t.Key === 'Name')?.Value : null;
|
|
|
|
vpcs.push({
|
|
label: name || obj.VpcId,
|
|
subLabel: name ? obj.VpcId : obj.CidrBlock,
|
|
isDefault: obj.IsDefault || false,
|
|
kind: 'vpc',
|
|
value: obj.VpcId,
|
|
});
|
|
}
|
|
|
|
vpcs = sortBy(vpcs, ['isDefault:desc', 'label']);
|
|
|
|
for ( const obj of this.subnetInfo.Subnets ) {
|
|
if ( obj.AvailabilityZone !== `${ this.value.region }${ this.value.zone }` ) {
|
|
continue;
|
|
}
|
|
|
|
let entry = subnetsByVpc[obj.VpcId];
|
|
|
|
if ( !entry ) {
|
|
entry = [];
|
|
subnetsByVpc[obj.VpcId] = entry;
|
|
}
|
|
|
|
const name = obj.Tags && obj.Tags?.length ? obj.Tags.find((t) => t.Key === 'Name')?.Value : null;
|
|
|
|
entry.push({
|
|
label: name || obj.SubnetId,
|
|
subLabel: name ? obj.SubnetId : obj.CidrBlock,
|
|
kind: 'subnet',
|
|
isDefault: obj.DefaultForAz || false,
|
|
value: obj.SubnetId,
|
|
vpcId: obj.VpcId,
|
|
});
|
|
}
|
|
|
|
const out = [];
|
|
|
|
for ( const obj of vpcs ) {
|
|
addObject(out, obj);
|
|
|
|
if ( subnetsByVpc[obj.value] ) {
|
|
addObjects(out, sortBy(subnetsByVpc[obj.value], ['isDefault:desc', 'label']));
|
|
}
|
|
}
|
|
|
|
return out;
|
|
},
|
|
|
|
securityGroupOptions() {
|
|
if ( !this.securityGroupInfo ) {
|
|
return [];
|
|
}
|
|
|
|
const out = this.securityGroupInfo.SecurityGroups.filter((obj) => {
|
|
return obj.VpcId === this.value.vpcId;
|
|
}).map((obj) => {
|
|
return {
|
|
label: obj.GroupName,
|
|
description: obj.GroupDescription,
|
|
value: obj.GroupName
|
|
};
|
|
});
|
|
|
|
return sortBy(out, 'label');
|
|
},
|
|
|
|
kmsOptions() {
|
|
if ( !this.kmsInfo ) {
|
|
return [];
|
|
}
|
|
|
|
const out = this.kmsInfo.Keys.map((obj) => {
|
|
return obj.KeyArn;
|
|
}).sort();
|
|
|
|
return out;
|
|
},
|
|
|
|
DEFAULT_GROUP() {
|
|
return DEFAULT_GROUP;
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
'credentialId'() {
|
|
this.$fetch();
|
|
},
|
|
|
|
'value.region'() {
|
|
this.updateNetwork();
|
|
this.$fetch();
|
|
},
|
|
|
|
'value.zone'() {
|
|
this.$fetch();
|
|
},
|
|
|
|
'securityGroupMode'(val) {
|
|
this.value.securityGroupReadonly = ( val !== 'default' );
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
stringify,
|
|
|
|
initNetwork() {
|
|
const id = this.value.subnetId || this.value.vpcId;
|
|
|
|
this.selectedNetwork = id;
|
|
},
|
|
|
|
updateNetwork(value) {
|
|
let obj;
|
|
|
|
if ( value ) {
|
|
obj = findBy(this.networkOptions, 'value', value);
|
|
}
|
|
|
|
if ( obj?.kind === 'subnet' ) {
|
|
this.value.subnetId = value;
|
|
this.value.vpcId = obj.vpcId;
|
|
this.selectedNetwork = value;
|
|
} else if ( obj ) {
|
|
this.value.subnetId = null;
|
|
this.value.vpcId = value;
|
|
this.selectedNetwork = value;
|
|
} else {
|
|
this.value.subnetId = null;
|
|
this.value.vpcId = null;
|
|
this.selectedNetwork = null;
|
|
}
|
|
},
|
|
|
|
initTags() {
|
|
this.tags = convertStringToKV(this.value.tags);
|
|
},
|
|
|
|
updateTags(tags) {
|
|
this.value['tags'] = convertKVToString(tags);
|
|
},
|
|
|
|
test() {
|
|
const errors = [];
|
|
|
|
if (!this.selectedNetwork) {
|
|
errors.push(this.t('validation.required', { key: 'VPC/Subnet' }, true));
|
|
}
|
|
|
|
return { errors };
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<Loading v-if="$fetchState.pending" />
|
|
<template v-else>
|
|
<div v-if="errors.length">
|
|
<div
|
|
v-for="(err, idx) in errors"
|
|
:key="idx"
|
|
>
|
|
<Banner
|
|
color="error"
|
|
:label="stringify(err)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loadedRegionalFor">
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<LabeledSelect
|
|
v-model:value="value.region"
|
|
:mode="mode"
|
|
:options="regionOptions"
|
|
:required="true"
|
|
:searchable="true"
|
|
:disabled="disabled"
|
|
:label="t('cluster.machineConfig.amazonEc2.region')"
|
|
/>
|
|
</div>
|
|
<div class="col span-6">
|
|
<LabeledSelect
|
|
v-model:value="value.zone"
|
|
:mode="mode"
|
|
:options="zoneOptions"
|
|
:required="true"
|
|
:disabled="disabled"
|
|
:label="t('cluster.machineConfig.amazonEc2.zone')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-20">
|
|
<div class="col span-9">
|
|
<LabeledSelect
|
|
v-model:value="value.instanceType"
|
|
:mode="mode"
|
|
:options="instanceOptions"
|
|
:required="true"
|
|
:selectable="option => !option.disabled"
|
|
:searchable="true"
|
|
:disabled="disabled"
|
|
:label="t('cluster.machineConfig.amazonEc2.instanceType')"
|
|
>
|
|
<template v-slot:option="opt">
|
|
<template v-if="opt.kind === 'group'">
|
|
<b>{{ opt.label }}</b>
|
|
</template>
|
|
<template v-else>
|
|
<span class="pl-10">{{ opt.label }}</span>
|
|
</template>
|
|
</template>
|
|
</LabeledSelect>
|
|
</div>
|
|
<div class="col span-3">
|
|
<UnitInput
|
|
v-model:value="value.rootSize"
|
|
output-as="string"
|
|
:mode="mode"
|
|
:disabled="disabled"
|
|
:placeholder="t('cluster.machineConfig.amazonEc2.rootSize.placeholder')"
|
|
:label="t('cluster.machineConfig.amazonEc2.rootSize.label')"
|
|
:suffix="t('cluster.machineConfig.amazonEc2.rootSize.suffix')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-20 mb-20">
|
|
<div class="col span-6">
|
|
<LabeledSelect
|
|
:mode="mode"
|
|
:value="selectedNetwork"
|
|
:options="networkOptions"
|
|
:searchable="true"
|
|
:required="true"
|
|
:disabled="disabled"
|
|
:placeholder="t('cluster.machineConfig.amazonEc2.selectedNetwork.placeholder')"
|
|
:label="t('cluster.machineConfig.amazonEc2.selectedNetwork.label')"
|
|
data-testid="amazonEc2__selectedNetwork"
|
|
option-key="value"
|
|
@update:value="updateNetwork($event)"
|
|
>
|
|
<template v-slot:option="opt">
|
|
<div :class="{'vpc': opt.kind === 'vpc', 'vpc-subnet': opt.kind !== 'vpc'}">
|
|
<span class="vpc-name">{{ opt.label }}</span><span class="vpc-info">{{ opt.subLabel }}</span>
|
|
</div>
|
|
</template>
|
|
</LabeledSelect>
|
|
</div>
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model:value="value.iamInstanceProfile"
|
|
:mode="mode"
|
|
:disabled="disabled"
|
|
:required="isIamInstanceProfileNameRequired"
|
|
:tooltip="t('cluster.machineConfig.amazonEc2.iamInstanceProfile.tooltip')"
|
|
:label="t('cluster.machineConfig.amazonEc2.iamInstanceProfile.label')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<portal :to="'advanced-'+uuid">
|
|
<div class="row mt-20">
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model:value="value.ami"
|
|
:mode="mode"
|
|
:disabled="disabled"
|
|
:placeholder="t('cluster.machineConfig.amazonEc2.ami.placeholder')"
|
|
:label="t('cluster.machineConfig.amazonEc2.ami.label')"
|
|
/>
|
|
</div>
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model:value="value.sshUser"
|
|
:mode="mode"
|
|
:label="t('cluster.machineConfig.amazonEc2.sshUser.label')"
|
|
:disabled="!value.ami || disabled"
|
|
:tooltip="t('cluster.machineConfig.amazonEc2.sshUser.tooltip')"
|
|
:placeholder="t('cluster.machineConfig.amazonEc2.sshUser.placeholder')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-20">
|
|
<div class="col span-12">
|
|
<h3>
|
|
{{ t('cluster.machineConfig.amazonEc2.securityGroup.title') }}
|
|
<span
|
|
v-if="!value.vpcId"
|
|
class="text-muted text-small"
|
|
>
|
|
{{ t('cluster.machineConfig.amazonEc2.securityGroup.vpcId') }}
|
|
</span>
|
|
</h3>
|
|
<RadioGroup
|
|
v-model:value="securityGroupMode"
|
|
name="securityGroupMode"
|
|
:mode="mode"
|
|
:disabled="!value.vpcId || disabled"
|
|
:labels="securityGroupLabels"
|
|
:options="['default','custom']"
|
|
/>
|
|
<LabeledSelect
|
|
v-if="value.vpcId && securityGroupMode === 'custom'"
|
|
v-model:value="value.securityGroup"
|
|
:mode="mode"
|
|
:disabled="!value.vpcId || disabled"
|
|
:options="securityGroupOptions"
|
|
:searchable="true"
|
|
:multiple="true"
|
|
:taggable="true"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-20">
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model:value="value.volumeType"
|
|
:mode="mode"
|
|
:disabled="disabled"
|
|
:label="t('cluster.machineConfig.amazonEc2.volumeType.label')"
|
|
:placeholder="t('cluster.machineConfig.amazonEc2.volumeType.placeholder')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-20">
|
|
<div class="col span-12">
|
|
<Checkbox
|
|
v-model:value="value.encryptEbsVolume"
|
|
:mode="mode"
|
|
:label="t('cluster.machineConfig.amazonEc2.encryptEbsVolume')"
|
|
/>
|
|
<div
|
|
v-if="value.encryptEbsVolume"
|
|
class="mt-10"
|
|
>
|
|
<LabeledSelect
|
|
v-if="canReadKms"
|
|
v-model:value="value.kmsKey"
|
|
:mode="mode"
|
|
:options="kmsOptions"
|
|
:disabled="disabled"
|
|
:label="t('cluster.machineConfig.amazonEc2.kmsKey.label')"
|
|
/>
|
|
<template v-else>
|
|
<LabeledInput
|
|
v-model:value="value.kmsKey"
|
|
:mode="mode"
|
|
:disabled="disabled"
|
|
:label="t('cluster.machineConfig.amazonEc2.kmsKey.label')"
|
|
/>
|
|
<p class="text-muted">
|
|
{{ t('cluster.machineConfig.amazonEc2.kmsKey.text') }}
|
|
</p>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-20">
|
|
<div class="col span-6">
|
|
<Checkbox
|
|
v-model:value="value.requestSpotInstance"
|
|
:mode="mode"
|
|
:label="t('cluster.machineConfig.amazonEc2.requestSpotInstance')"
|
|
/>
|
|
<div
|
|
v-if="value.requestSpotInstance"
|
|
class="mt-10"
|
|
>
|
|
<UnitInput
|
|
v-model:value="value.spotPrice"
|
|
output-as="string"
|
|
:mode="mode"
|
|
:disabled="disabled"
|
|
:placeholder="t('cluster.machineConfig.amazonEc2.spotPrice.placeholder')"
|
|
:label="t('cluster.machineConfig.amazonEc2.spotPrice.label')"
|
|
:suffix="t('cluster.machineConfig.amazonEc2.spotPrice.suffix')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-20">
|
|
<div class="col span-12">
|
|
<div>
|
|
<Checkbox
|
|
v-model:value="value.privateAddressOnly"
|
|
:mode="mode"
|
|
:disabled="disabled"
|
|
:label="t('cluster.machineConfig.amazonEc2.privateAddressOnly')"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Checkbox
|
|
v-model:value="value.useEbsOptimizedInstance"
|
|
:mode="mode"
|
|
:disabled="disabled"
|
|
:label="t('cluster.machineConfig.amazonEc2.useEbsOptimizedInstance')"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Checkbox
|
|
v-model:value="value.httpEndpoint"
|
|
value-when-true="enabled"
|
|
:mode="mode"
|
|
:disabled="disabled"
|
|
:label="t('cluster.machineConfig.amazonEc2.httpEndpoint')"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Checkbox
|
|
v-model:value="value.httpTokens"
|
|
value-when-true="required"
|
|
:mode="mode"
|
|
:disabled="!value.httpEndpoint || disabled"
|
|
:label="t('cluster.machineConfig.amazonEc2.httpTokens')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-20">
|
|
<div class="col span-12">
|
|
<KeyValue
|
|
:value="tags"
|
|
:mode="mode"
|
|
:read-allowed="false"
|
|
:label="t('cluster.machineConfig.amazonEc2.tagTitle')"
|
|
:add-label="t('labels.addTag')"
|
|
:disabled="disabled"
|
|
@update:value="updateTags"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</portal>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
<style scoped lang="scss">
|
|
.vpc, .vpc-subnet {
|
|
display: flex;
|
|
line-height: 30px;
|
|
|
|
.vpc-name {
|
|
font-weight: bold;
|
|
flex: 1;
|
|
}
|
|
|
|
.vpc-info {
|
|
font-size: 12px;
|
|
opacity: 0.7;
|
|
}
|
|
}
|
|
|
|
.vpc-subnet .vpc-name {
|
|
font-weight: normal;
|
|
padding-left: 15px;
|
|
}
|
|
</style>
|