dashboard/shell/edit/autoscaling.horizontalpodau.../index.vue

347 lines
9.8 KiB
Vue

<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import CruResource from '@shell/components/CruResource';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import Labels from '@shell/components/form/Labels';
import Loading from '@shell/components/Loading';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import Tab from '@shell/components/Tabbed/Tab';
import Tabbed from '@shell/components/Tabbed';
import MetricsRow from '@shell/edit/autoscaling.horizontalpodautoscaler/metrics-row';
import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
import { DEFAULT_RESOURCE_METRIC } from '@shell/edit/autoscaling.horizontalpodautoscaler/resource-metric';
import { Checkbox } from '@components/Form/Checkbox';
import { API_SERVICE, SCALABLE_WORKLOAD_TYPES } from '@shell/config/types';
import isEmpty from 'lodash/isEmpty';
import find from 'lodash/find';
import endsWith from 'lodash/endsWith';
import { findBy } from '@shell/utils/array';
import HpaScalingRule from '@shell/edit/autoscaling.horizontalpodautoscaler/hpa-scaling-rule.vue';
const RESOURCE_METRICS_API_GROUP = 'metrics.k8s.io';
export default {
name: 'CruHPA',
emits: ['input'],
components: {
HpaScalingRule,
ArrayListGrouped,
Checkbox,
CruResource,
LabeledInput,
LabeledSelect,
Labels,
Loading,
NameNsDescription,
MetricsRow,
Tab,
Tabbed,
},
mixins: [CreateEditView],
props: {
value: {
type: Object,
required: true,
},
mode: {
type: String,
default: 'create',
},
},
fetch() {
const promises = [this.loadAPIServices(), this.loadWorkloads()];
return Promise.all(promises);
},
data() {
return { defaultResourceMetric: DEFAULT_RESOURCE_METRIC };
},
computed: {
allMetrics() {
return this.value?.spec?.metrics;
},
allWorkloadsFiltered() {
return (
Object.values(SCALABLE_WORKLOAD_TYPES)
.flatMap((type) => this.$store.getters['cluster/all'](type))
.filter(
// Filter out anything that has an owner, which should probably be the one with the HPA
// For example ReplicaSets can be associated with a HPA (https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/#replicaset-as-a-horizontal-pod-autoscaler-target)
// but wouldn't make sense if it's owned by a deployment
(wl) => wl.metadata.namespace === this.value.metadata.namespace && !wl.ownedByWorkload
)
);
},
allWorkloadsMapped() {
return this.allWorkloadsFiltered
// Update to type OBJECT_REFERENCE which can be stored directly as scaleTargetRef
.map((workload) => ({
kind: workload.kind,
name: workload.metadata.name,
apiVersion: workload.apiVersion,
}));
},
allServices() {
return this.$store.getters['cluster/all'](API_SERVICE);
},
resourceMetricsAvailable() {
const { allServices } = this;
return !isEmpty(
find(
allServices,
(api) => api.name.split('.').length === 4 &&
endsWith(api.name, RESOURCE_METRICS_API_GROUP)
)
);
},
selectedTargetRef() {
const { scaleTargetRef: { name } } = this.value.spec;
const { allWorkloadsFiltered } = this;
const match = findBy(allWorkloadsFiltered, 'metadata.name', name);
return match ?? null;
},
hasScaleDownRules: {
get() {
return !!this.value.spec.behavior?.scaleDown;
},
set(hasScaleDownRules) {
if (hasScaleDownRules) {
if (!this.value.spec.behavior) {
this.value.spec['behavior'] = {};
}
if (!this.value.spec.behavior?.scaleDown) {
this.value.spec.behavior['scaleDown'] = {};
}
} else {
delete this.value.spec.behavior['scaleDown'];
}
}
},
hasScaleUpRules: {
get() {
return !!this.value.spec.behavior?.scaleUp;
},
set(hasScaleUpRules) {
if (hasScaleUpRules) {
if (!this.value.spec.behavior) {
this.value.spec['behavior'] = {};
}
if (!this.value.spec.behavior?.scaleUp) {
this.value.spec.behavior['scaleUp'] = {};
}
} else {
delete this.value.spec.behavior['scaleUp'];
}
}
},
},
created() {
if (!this?.value?.spec?.type) {
if (!this.value?.spec) {
this.initSpec();
}
}
},
methods: {
initSpec() {
this.value['spec'] = {
type: 'io.k8s.api.autoscaling.v1.horizontalpodautoscalerspec',
minReplicas: 1,
maxReplicas: 10,
scaleTargetRef: {
apiVersion: '',
kind: '',
name: '',
},
metrics: [{ ...this.defaultResourceMetric }]
};
},
async loadWorkloads() {
await Promise.all(
Object.values(SCALABLE_WORKLOAD_TYPES).map((type) => this.$store.dispatch('cluster/findAll', { type })
)
);
},
async loadAPIServices() {
await this.$store.dispatch('cluster/findAll', { type: API_SERVICE });
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<form
v-else
class="filled-height"
>
<CruResource
:done-route="doneRoute"
:mode="mode"
:resource="value"
:validation-passed="true"
:errors="errors"
@error="(e) => (errors = e)"
@finish="save"
@cancel="done"
>
<NameNsDescription
v-if="!isView"
:value="value"
:mode="mode"
/>
<Tabbed
:side-tabs="true"
:use-hash="useTabbedHash"
>
<Tab
name="target"
:label="t('hpa.tabs.target')"
:weight="10"
>
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model:value="value.spec.scaleTargetRef"
:get-option-label="(opt) => opt.name"
:mode="mode"
:label="t('hpa.workloadTab.targetReference')"
:options="allWorkloadsMapped"
:required="true"
>
<template v-slot:option="option">
{{ option.name }}<span class="pull-right">{{ option.kind }}</span>
</template>
</LabeledSelect>
</div>
</div>
<div class="row">
<div class="col span-6">
<LabeledInput
v-model:value.number="value.spec.minReplicas"
:mode="mode"
:label="t('hpa.workloadTab.min')"
placeholder="1"
:required="true"
type="number"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value.number="value.spec.maxReplicas"
:mode="mode"
:label="t('hpa.workloadTab.max')"
placeholder="1"
:required="true"
type="number"
/>
</div>
</div>
</Tab>
<Tab
name="metrics"
:label="t('hpa.tabs.metrics')"
>
<ArrayListGrouped
v-model:value="value.spec.metrics"
:default-add-value="{ ...defaultResourceMetric }"
:mode="mode"
:initial-empty-row="true"
>
<template #remove-button="removeProps">
<button
v-if="value.spec.metrics.length > 1"
type="button"
class="btn role-link close btn-sm"
@click="removeProps.remove"
>
<i class="icon icon-x" />
</button>
<span v-else />
</template>
<template #default="props">
<MetricsRow
v-model:value="props.row.value"
:mode="mode"
:metrics-available="resourceMetricsAvailable"
:referent="selectedTargetRef"
/>
</template>
</ArrayListGrouped>
</Tab>
<Tab
name="behavior"
:label="t('hpa.tabs.behavior')"
>
<div class="col span-12 mb-10">
<h3>
{{ t('hpa.scaleDownRules.label') }}
</h3>
<div class="row mb-10">
<Checkbox
v-model:value="hasScaleDownRules"
:mode="mode"
:label="t('hpa.scaleDownRules.enable')"
/>
</div>
<HpaScalingRule
v-if="hasScaleDownRules"
:value="value"
type="scaleDown"
:mode="mode"
@update:value="$emit('input', $event)"
/>
</div>
<div class="col span-12">
<h3>
{{ t('hpa.scaleUpRules.label') }}
</h3>
<div class="row mb-10">
<Checkbox
v-model:value="hasScaleUpRules"
:mode="mode"
:label="t('hpa.scaleUpRules.enable')"
/>
</div>
<HpaScalingRule
v-if="hasScaleUpRules"
:value="value"
type="scaleUp"
:mode="mode"
@update:value="$emit('input', $event)"
/>
</div>
</Tab>
<Tab
name="labels-and-annotations"
:label="t('hpa.tabs.labels')"
:weight="-1"
>
<Labels
:default-container-class="'labels-and-annotations-container'"
:value="value"
:mode="mode"
:display-side-by-side="false"
/>
</Tab>
</Tabbed>
</CruResource>
</form>
</template>