mirror of https://github.com/rancher/dashboard.git
515 lines
12 KiB
Vue
515 lines
12 KiB
Vue
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import debounce from 'lodash/debounce';
|
|
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
|
import { removeAt, findBy } from '@shell/utils/array';
|
|
import { clone } from '@shell/utils/object';
|
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
import { HCI as HCI_LABELS_ANNOTATIONS } from '@shell/config/labels-annotations';
|
|
import { isHarvesterSatisfiesVersion } from '@shell/utils/cluster';
|
|
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
|
import { CAPI, SERVICE } from '@shell/config/types';
|
|
|
|
export default {
|
|
emits: ['update:value'],
|
|
|
|
components: {
|
|
LabeledInput,
|
|
LabeledSelect,
|
|
},
|
|
|
|
props: {
|
|
value: {
|
|
type: Array,
|
|
default: null,
|
|
},
|
|
|
|
mode: {
|
|
type: String,
|
|
default: _EDIT,
|
|
},
|
|
|
|
// array of services auto-created previously (only relevent when mode !== create)
|
|
services: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
|
|
// workload name
|
|
name: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
rows: [],
|
|
showHostPorts: false,
|
|
workloadPortOptions: ['TCP', 'UDP']
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters(['currentCluster']),
|
|
|
|
canNotAccessService() {
|
|
return !this.$store.getters['cluster/schemaFor'](SERVICE);
|
|
},
|
|
|
|
serviceTypeTooltip() {
|
|
return this.canNotAccessService ? this.t('workload.container.noServiceAccess') : undefined;
|
|
},
|
|
|
|
isView() {
|
|
return this.mode === _VIEW;
|
|
},
|
|
|
|
showAdd() {
|
|
return !this.isView;
|
|
},
|
|
|
|
showRemove() {
|
|
return !this.isView;
|
|
},
|
|
|
|
serviceTypes() {
|
|
return [
|
|
{
|
|
label: this.t('workload.container.ports.noCreateService'),
|
|
value: ''
|
|
},
|
|
{
|
|
label: this.t('serviceTypes.clusterip'),
|
|
value: 'ClusterIP'
|
|
},
|
|
{
|
|
label: this.t('serviceTypes.nodeport'),
|
|
value: 'NodePort'
|
|
},
|
|
{
|
|
label: this.t('serviceTypes.loadbalancer'),
|
|
value: 'LoadBalancer'
|
|
},
|
|
];
|
|
},
|
|
|
|
clusterIPServicePorts() {
|
|
return ((this.services.filter((svc) => svc.spec.type === 'ClusterIP') || [])[0] || {})?.spec?.ports;
|
|
},
|
|
|
|
loadBalancerServicePorts() {
|
|
return ((this.services.filter((svc) => svc.spec.type === 'LoadBalancer') || [])[0] || {})?.spec?.ports;
|
|
},
|
|
|
|
nodePortServicePorts() {
|
|
return ((this.services.filter((svc) => svc.spec.type === 'NodePort') || [])[0] || {})?.spec?.ports;
|
|
},
|
|
|
|
ipamOptions() {
|
|
return [{
|
|
label: 'DHCP',
|
|
value: 'dhcp',
|
|
}, {
|
|
label: 'Pool',
|
|
value: 'pool',
|
|
}];
|
|
},
|
|
|
|
ipamIndex() {
|
|
return this.rows.findIndex((row) => row._serviceType === 'LoadBalancer' && row.protocol === 'TCP');
|
|
},
|
|
|
|
serviceWithIpam() {
|
|
return this.services.find((s) => s?.metadata?.annotations[HCI_LABELS_ANNOTATIONS.CLOUD_PROVIDER_IPAM]);
|
|
},
|
|
|
|
showIpam() {
|
|
let cloudProvider;
|
|
const version = this.provisioningCluster?.kubernetesVersion;
|
|
|
|
if (this.provisioningCluster?.isRke2) {
|
|
const machineSelectorConfig = this.provisioningCluster?.spec?.rkeConfig?.machineSelectorConfig || {};
|
|
const agentConfig = (machineSelectorConfig[0] || {}).config;
|
|
|
|
cloudProvider = agentConfig?.['cloud-provider-name'];
|
|
} else if (this.provisioningCluster?.isRke1) {
|
|
const currentCluster = this.$store.getters['currentCluster'];
|
|
|
|
cloudProvider = currentCluster?.spec?.rancherKubernetesEngineConfig?.cloudProvider?.name;
|
|
}
|
|
|
|
return cloudProvider === HARVESTER &&
|
|
isHarvesterSatisfiesVersion(version);
|
|
},
|
|
|
|
provisioningCluster() {
|
|
const out = this.$store.getters['management/all'](CAPI.RANCHER_CLUSTER).find((c) => c?.status?.clusterName === this.currentCluster.metadata.name);
|
|
|
|
return out;
|
|
},
|
|
},
|
|
|
|
created() {
|
|
const rows = clone(this.value || []).map((row) => {
|
|
row._showHost = false;
|
|
row._serviceType = row._serviceType || '';
|
|
row._name = row.name ? `${ row.name }` : `${ row.containerPort }${ row.protocol.toLowerCase() }${ row.hostPort || row._listeningPort || '' }`;
|
|
if (row.hostPort || row.hostIP) {
|
|
row._showHost = true;
|
|
}
|
|
|
|
row._ipam = '';
|
|
|
|
return row;
|
|
});
|
|
|
|
this.rows = rows;
|
|
// show host port column if existing port data has any host ports defined
|
|
this.showHostPorts = !!rows.some((row) => !!row.hostPort);
|
|
this.queueUpdate = debounce(this.update, 500);
|
|
this.rows.map((row) => {
|
|
this.setServiceType(row);
|
|
this.setIpam(row);
|
|
});
|
|
},
|
|
|
|
methods: {
|
|
add() {
|
|
this.rows.push({
|
|
name: '',
|
|
expose: true,
|
|
protocol: 'TCP',
|
|
containerPort: null,
|
|
hostPort: null,
|
|
hostIP: null,
|
|
_showHost: false,
|
|
_serviceType: '',
|
|
_ipam: 'dhcp',
|
|
});
|
|
|
|
this.queueUpdate();
|
|
|
|
this.$nextTick(() => {
|
|
const inputs = this.$refs.name;
|
|
|
|
inputs[inputs.length - 1].focus();
|
|
});
|
|
},
|
|
|
|
remove(idx) {
|
|
removeAt(this.rows, idx);
|
|
this.queueUpdate();
|
|
},
|
|
|
|
update() {
|
|
if ( this.isView ) {
|
|
return;
|
|
}
|
|
const out = [];
|
|
|
|
for ( const row of this.rows ) {
|
|
const value = clone(row);
|
|
|
|
delete value._showHost;
|
|
out.push(value);
|
|
}
|
|
this.$emit('update:value', out);
|
|
},
|
|
|
|
setServiceType(row) {
|
|
const { _name } = row;
|
|
|
|
if (this.loadBalancerServicePorts) {
|
|
const portSpec = findBy(this.loadBalancerServicePorts, 'name', _name);
|
|
|
|
if (portSpec) {
|
|
row['_listeningPort'] = portSpec.port;
|
|
|
|
row._serviceType = 'LoadBalancer';
|
|
|
|
return;
|
|
}
|
|
} if (this.nodePortServicePorts) {
|
|
const portSpec = findBy(this.nodePortServicePorts, 'name', _name);
|
|
|
|
if (portSpec) {
|
|
row['_listeningPort'] = portSpec.nodePort;
|
|
|
|
row._serviceType = 'NodePort';
|
|
|
|
return;
|
|
}
|
|
} if (this.clusterIPServicePorts) {
|
|
if (findBy(this.clusterIPServicePorts, 'name', _name)) {
|
|
row._serviceType = 'ClusterIP';
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
},
|
|
|
|
setIpam(row) {
|
|
if (this.serviceWithIpam && row._serviceType === 'LoadBalancer' && row.protocol === 'TCP') {
|
|
row._ipam = this.serviceWithIpam?.metadata?.annotations[HCI_LABELS_ANNOTATIONS.CLOUD_PROVIDER_IPAM];
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div :style="{'width':'100%'}">
|
|
<p
|
|
v-if="rows.length > 0"
|
|
class="padded"
|
|
>
|
|
{{ t('workload.container.ports.detailedDescription') }}
|
|
</p>
|
|
<div
|
|
v-for="(row, idx) in rows"
|
|
:key="idx"
|
|
class="ports-row"
|
|
:class="{
|
|
'show-host':row._showHost,
|
|
'loadBalancer': row._serviceType === 'LoadBalancer',
|
|
'tcp': row.protocol === 'TCP',
|
|
'show-ipam': showIpam,
|
|
}"
|
|
>
|
|
<div class="service-type">
|
|
<LabeledSelect
|
|
v-model:value="row._serviceType"
|
|
:mode="mode"
|
|
:label="t('workload.container.ports.createService')"
|
|
:options="serviceTypes"
|
|
:disabled="canNotAccessService"
|
|
:tooltip="serviceTypeTooltip"
|
|
@update:value="queueUpdate"
|
|
/>
|
|
</div>
|
|
|
|
<div class="portName">
|
|
<LabeledInput
|
|
ref="name"
|
|
v-model:value="row.name"
|
|
:mode="mode"
|
|
:label="t('workload.container.ports.name')"
|
|
@update:value="queueUpdate"
|
|
/>
|
|
</div>
|
|
|
|
<div class="port">
|
|
<LabeledInput
|
|
v-model:value.number="row.containerPort"
|
|
:mode="mode"
|
|
type="number"
|
|
min="1"
|
|
max="65535"
|
|
placeholder="e.g. 8080"
|
|
:label="t('workload.container.ports.containerPort')"
|
|
:required="row._serviceType === 'LoadBalancer' "
|
|
@update:value="queueUpdate"
|
|
/>
|
|
</div>
|
|
|
|
<div class="protocol col">
|
|
<LabeledSelect
|
|
v-model:value="row.protocol"
|
|
:mode="mode"
|
|
:options="workloadPortOptions"
|
|
:multiple="false"
|
|
:label="t('workload.container.ports.protocol')"
|
|
@update:value="queueUpdate"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="row._showHost"
|
|
class="targetPort"
|
|
>
|
|
<LabeledInput
|
|
ref="port"
|
|
v-model:value.number="row.hostPort"
|
|
:mode="mode"
|
|
type="number"
|
|
min="1"
|
|
max="65535"
|
|
placeholder="e.g. 80"
|
|
:label="t('workload.container.ports.hostPort')"
|
|
@update:value="queueUpdate"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="row._showHost"
|
|
class="hostip"
|
|
>
|
|
<LabeledInput
|
|
ref="port"
|
|
v-model:value="row.hostIP"
|
|
:mode="mode"
|
|
placeholder="e.g. 1.1.1.1"
|
|
:label="t('workload.container.ports.hostIP')"
|
|
@update:value="queueUpdate"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="!row._showHost && row._serviceType !== 'LoadBalancer' && row._serviceType !== 'NodePort'"
|
|
class="add-host"
|
|
>
|
|
<button
|
|
:disabled="mode==='view'"
|
|
type="button"
|
|
class="btn btn-sm role-tertiary"
|
|
@click="row._showHost = true"
|
|
>
|
|
{{ t('workloadPorts.addHost') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="row._serviceType === 'LoadBalancer' || row._serviceType === 'NodePort'">
|
|
<LabeledInput
|
|
ref="port"
|
|
v-model:value.number="row._listeningPort"
|
|
type="number"
|
|
:mode="mode"
|
|
:label="t('workload.container.ports.listeningPort')"
|
|
:required="row._serviceType === 'LoadBalancer' "
|
|
@update:value="queueUpdate"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="showIpam && row._serviceType === 'LoadBalancer' && row.protocol === 'TCP'">
|
|
<div v-if="idx === ipamIndex">
|
|
<LabeledSelect
|
|
v-model:value="row._ipam"
|
|
:mode="mode"
|
|
:options="ipamOptions"
|
|
:label="t('servicesPage.harvester.ipam.label')"
|
|
:disabled="mode === 'edit'"
|
|
@update:value="queueUpdate"
|
|
/>
|
|
</div>
|
|
<div v-else>
|
|
<LabeledSelect
|
|
v-model:value="rows[ipamIndex]._ipam"
|
|
:mode="mode"
|
|
:options="ipamOptions"
|
|
:label="t('servicesPage.harvester.ipam.label')"
|
|
:disabled="true"
|
|
@update:value="queueUpdate"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="showRemove"
|
|
class="remove"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn role-link"
|
|
@click="remove(idx)"
|
|
>
|
|
{{ t('workloadPorts.remove') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="showAdd"
|
|
class="footer"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn role-tertiary add"
|
|
@click="add()"
|
|
>
|
|
{{ t('workloadPorts.addPort') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
$remove: 75;
|
|
$checkbox: 75;
|
|
|
|
.title {
|
|
margin-bottom: 10px;
|
|
|
|
.read-from-file {
|
|
float: right;
|
|
}
|
|
}
|
|
.ports-headers, .ports-row{
|
|
display: grid;
|
|
grid-template-columns: 28% 28% 15% 10% 75px 0.5fr;
|
|
grid-column-gap: $column-gutter;
|
|
margin-bottom: 10px;
|
|
align-items: center;
|
|
& .port{
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
&.show-host{
|
|
grid-template-columns: 20% 20% 145px 90px 140px .5fr .5fr;
|
|
}
|
|
|
|
&.show-ipam.loadBalancer.tcp{
|
|
grid-template-columns: 20% 20% 145px 90px .5fr 140px .5fr;
|
|
}
|
|
|
|
&.show-ipam.show-host.loadBalancer{
|
|
grid-template-columns: 20% 10% 135px 90px 105px .5fr .5fr .5fr;
|
|
}
|
|
|
|
&.show-ipam.show-host.loadBalancer.tcp{
|
|
grid-template-columns: 12% 10% 135px 90px 105px .5fr .5fr 100px .5fr;
|
|
}
|
|
}
|
|
|
|
.add-host {
|
|
justify-self: center;
|
|
}
|
|
|
|
.protocol {
|
|
height: 100%;
|
|
}
|
|
|
|
.ports-headers {
|
|
color: var(--input-label);
|
|
}
|
|
|
|
.toggle-host-ports {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.remove BUTTON {
|
|
padding: 0px;
|
|
}
|
|
|
|
.ports-row INPUT {
|
|
height: 50px;
|
|
}
|
|
|
|
.footer {
|
|
.protip {
|
|
float: right;
|
|
padding: 5px 0;
|
|
}
|
|
}
|
|
.ports-row .protocol :deep() .unlabeled-select,
|
|
.ports-row .protocol :deep() .unlabeled-select .v-select {
|
|
height: 100%;
|
|
}
|
|
.ports-row .protocol :deep() .unlabeled-select .vs__dropdown-toggle {
|
|
padding-top: 12px;
|
|
}
|
|
</style>
|