dashboard/machine-config/amazonec2.vue

634 lines
17 KiB
Vue

<script>
import Loading from '@/components/Loading';
import Banner from '@/components/Banner';
import CreateEditView from '@/mixins/create-edit-view';
import LabeledSelect from '@/components/form/LabeledSelect';
import LabeledInput from '@/components/form/LabeledInput';
import KeyValue from '@/components/form/KeyValue';
import UnitInput from '@/components/form/UnitInput';
import RadioGroup from '@/components/form/RadioGroup';
import Checkbox from '@/components/form/Checkbox';
import { NORMAN } from '@/config/types';
import { allHash } from '@/utils/promise';
import { addObject, addObjects, findBy } from '@/utils/array';
import { sortBy } from '@/utils/sort';
import { stringify, exceptionToErrorsArray } from '@/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,
},
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 });
}
if ( !this.instanceInfo ) {
this.instanceInfo = await this.$store.dispatch('aws/instanceInfo');
}
const region = this.value.region || this.credential?.decodedData.defaultRegion || this.$store.getters['aws/defaultRegion'];
const cloudCredentialId = this.credential.id;
if ( !this.value.region ) {
this.$set(this.value, 'region', region);
}
this.ec2Client = await this.$store.dispatch('aws/ec2', { region, cloudCredentialId });
this.kmsClient = await this.$store.dispatch('aws/kms', { region, cloudCredentialId });
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.$set(this.value, 'zone', 'a');
}
if ( !this.value.instanceType ) {
this.$set(this.value, 'instanceType', this.$store.getters['aws/defaultInstanceType']);
}
this.initNetwork();
this.initTags();
if ( !this.value.securityGroup?.length ) {
this.$set(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: {
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 ) {
vpcs.push({
label: `${ 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;
}
entry.push({
label: `${ obj.SubnetId } (${ obj.CidrBlock } - ${ obj.AvailableIpAddressCount } available)`,
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.$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() {
const parts = (this.value.tags || '').split(/,/);
const out = {};
let i = 0;
while ( i + 1 < parts.length ) {
const key = `${ parts[i] }`.trim();
const value = `${ parts[i + 1] }`.trim();
if ( key ) {
out[key] = value;
}
i += 2;
}
this.tags = out;
},
updateTags(tags) {
const ary = [];
for ( const k in tags ) {
ary.push(k, tags[k]);
}
this.$set(this.value, 'tags', ary.join(','));
},
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" />
<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-if="loadedRegionalFor">
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model="value.region"
:mode="mode"
:options="regionOptions"
:required="true"
:searchable="true"
:disabled="disabled"
label="Region"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model="value.zone"
:mode="mode"
:options="zoneOptions"
:required="true"
:disabled="disabled"
label="Zone"
/>
</div>
</div>
<div class="row mb-20">
<div class="col span-9">
<LabeledSelect
v-model="value.instanceType"
:mode="mode"
:options="instanceOptions"
:required="true"
:selectable="option => !option.disabled"
:searchable="true"
:disabled="disabled"
label="Instance Type"
>
<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.rootSize"
output-as="string"
:mode="mode"
:disabled="disabled"
placeholder="Default: 16"
label="Root Disk Size"
suffix="GB"
/>
</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"
label="VPC/Subnet"
placeholder="Select a VPC or Subnet"
@input="updateNetwork($event)"
>
<template v-slot:option="opt">
<template v-if="opt.kind === 'vpc'">
<b>{{ opt.label }}</b>
</template>
<template v-else>
<span class="pl-10">{{ opt.label }}</span>
</template>
</template>
</LabeledSelect>
</div>
<div class="col span-6">
<LabeledInput
v-model="value.iamInstanceProfile"
:mode="mode"
:disabled="disabled"
label="IAM Instance Profile Name"
tooltip="Kubernetes AWS Cloud Provider support requires an appropriate instance profile"
/>
</div>
</div>
<portal :to="'advanced-'+uuid">
<div class="row mt-20">
<div class="col span-6">
<LabeledInput
v-model="value.ami"
:mode="mode"
:disabled="disabled"
label="AMI ID"
placeholder="Default: A recent Ubuntu LTS"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model="value.sshUser"
:mode="mode"
label="SSH Username for AMI"
:disabled="!value.ami || disabled"
placeholder="Default: ubuntu"
tooltip="The username that exists in the selected AMI; Provisioning will SSH to the node with this."
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-12">
<h3>
Security Group
<span v-if="!value.vpcId" class="text-muted text-small">
(select a VPC/Subnet first)
</span>
</h3>
<RadioGroup
v-model="securityGroupMode"
name="securityGroupMode"
:mode="mode"
:disabled="!value.vpcId || disabled"
:labels="[`Standard: Automatically create and use a &quot;${DEFAULT_GROUP}&quot; security group`, 'Choose one or more existing security groups:']"
:options="['default','custom']"
/>
<LabeledSelect
v-if="value.vpcId && securityGroupMode === 'custom'"
v-model="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.volumeType"
:mode="mode"
:disabled="disabled"
label="EBS Root Volume Type"
placeholder="Default: gp2"
/>
</div>
</div>
<div class="row mt-20">
<div class="col span-12">
<Checkbox v-model="value.encryptEbsVolume" :mode="mode" label="Encrypt EBS Volume" />
<div v-if="value.encryptEbsVolume" class="mt-10">
<LabeledSelect
v-if="canReadKms"
v-model="value.kmsKey"
:mode="mode"
:options="kmsOptions"
:disabled="disabled"
label="KMS Key ARN"
/>
<template v-else>
<LabeledInput
v-model="value.kmsKey"
:mode="mode"
:disabled="disabled"
label="KMS Key ARN"
/>
<p class="text-muted">
You do not have permission to list KMS keys, but may still be able to enter a Key ARN if you know one.
</p>
</template>
</div>
</div>
</div>
<div class="row mt-20">
<div class="col span-6">
<Checkbox v-model="value.requestSpotInstance" :mode="mode" label="Request Spot Instance" />
<div v-if="value.requestSpotInstance" class="mt-10">
<UnitInput
v-model="value.spotPrice"
output-as="string"
:mode="mode"
:disabled="disabled"
placeholder="Default: 0.50"
label="Spot Price"
suffix="Dollars per hour"
/>
</div>
</div>
</div>
<div class="row mt-20">
<div class="col span-12">
<div>
<Checkbox
v-model="value.privateAddressOnly"
:mode="mode"
:disabled="disabled"
label="Use only private addresses"
/>
</div>
<div>
<Checkbox
v-model="value.useEbsOptimizedInstance"
:mode="mode"
:disabled="disabled"
label="EBS-Optimized Instance"
/>
</div>
<div>
<Checkbox
v-model="value.httpEndpoint"
:mode="mode"
:disabled="disabled"
label="Allow access to EC2 metadata"
/>
</div>
<div>
<Checkbox
v-model="value.httpTokens"
:mode="mode"
:disabled="!value.httpEndpoint || disabled"
label="Use tokens for metadata"
/>
</div>
</div>
</div>
<div class="row mt-20">
<div class="col span-12">
<KeyValue
:value="tags"
:mode="mode"
:read-allowed="false"
title="EC2 Tags"
:add-label="t('labels.addTag')"
:disabled="disabled"
@input="updateTags"
/>
</div>
</div>
</portal>
</div>
</div>
</template>