dashboard/shell/edit/workload/index.vue

674 lines
21 KiB
Vue

<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import FormValidation from '@shell/mixins/form-validation';
import WorkLoadMixin from '@shell/edit/workload/mixins/workload';
import { mapGetters } from 'vuex';
export default {
name: 'Workload',
emits: ['input'],
mixins: [CreateEditView, FormValidation, WorkLoadMixin], // The order here is important since WorkLoadMixin contains some FormValidation configuration
props: {
value: {
type: Object,
required: true,
},
mode: {
type: String,
default: 'create',
},
},
data() {
return { selectedName: null, errors: [] };
},
computed: { ...mapGetters({ t: 'i18n/t' }) },
methods: {
changed(tab) {
const key = this.idKey;
this.selectedName = tab.selectedName;
const container = this.containerOptions.find( (c) => c[key] === tab.selectedName);
if ( container ) {
this.selectContainer(container);
}
},
/**
* Find error exceptions to be mapped for each case
*/
mapError(error) {
const isObject = error && typeof error === 'object' && !Array.isArray(error);
// We have just 2 cases, so we'll set a ternary operation:
// - one is for when we submit a YAML edited form (object - YAMLexceptions)
// - other is for a string message
const errorMessage = isObject ? error.message || '' : error || '';
switch (true) {
case errorMessage.includes('violates PodSecurity'): {
const match = errorMessage.match(/\"(.*?)\"/gi);
const name = match[0];
const policy = match[1];
return { message: this.t('workload.error', { name, policy }) };
}
default:
break;
}
},
/**
* Map all the error texts to a message and icon object
*/
getErrorsMap(errors) {
return !errors || !Array.isArray(errors) ? {} : errors.reduce((acc, error) => ({
...acc,
[error]: this.mapError(error) || {
message: error,
icon: null
}
}), {});
}
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<form
v-else
class="filled-height"
>
<CruResource
:validation-passed="fvFormIsValid"
:selected-subtype="type"
:resource="value"
:mode="mode"
:errors="errors"
:done-route="doneRoute"
:subtypes="workloadSubTypes"
:apply-hooks="applyHooks"
:value="value"
:errors-map="getErrorsMap(fvUnreportedValidationErrors)"
@finish="save"
@select-type="selectType"
@error="e=>errors = e"
>
<NameNsDescription
:value="value"
:mode="mode"
:rules="{name: fvGetAndReportPathRules('metadata.name'), namespace: fvGetAndReportPathRules('metadata.namespace'), description: []}"
@change="name=value.metadata.name"
@isNamespaceNew="isNamespaceNew = $event"
/>
<div
v-if="isCronJob || isReplicable || isStatefulSet || containerOptions.length > 1"
class="row mb-20"
>
<div
v-if="isCronJob"
class="col span-3"
>
<LabeledInput
v-model:value="spec.schedule"
type="cron"
required
:mode="mode"
:label="t('workload.cronSchedule')"
:rules="fvGetAndReportPathRules('spec.schedule')"
placeholder="0 * * * *"
/>
</div>
<div
v-if="isReplicable"
class="col span-3"
>
<LabeledInput
v-model:value.number="spec.replicas"
type="number"
min="0"
required
:mode="mode"
:label="t('workload.replicas')"
/>
</div>
<div
v-if="isStatefulSet"
class="col span-3"
>
<LabeledSelect
v-model:value="spec.serviceName"
option-label="metadata.name"
:reduce="service=>service.metadata.name"
:mode="mode"
:label="t('workload.serviceName')"
:options="headlessServices"
required
/>
</div>
</div>
<Tabbed
ref="containersTabbed"
class="deployment-tabs"
:show-tabs-add-remove="true"
:default-tab="defaultTab"
:flat="true"
:use-hash="useTabbedHash"
data-testid="workload-horizontal-tabs"
@changed="changed"
>
<Tab
v-for="(tab, i) in allContainers"
:key="i"
:label="tab.name"
:name="tab[idKey]"
:weight="tab.weight"
:error="!!tab.error"
>
<Tabbed
:side-tabs="true"
:weight="99"
:data-testid="`workload-container-tabs-${i}`"
:use-hash="useTabbedHash"
>
<Tab
:label="t('workload.container.titles.general')"
name="general"
:weight="tabWeightMap['general']"
:error="tabErrors.general"
>
<template
#tab-header-right
>
<button
v-if="allContainers.length > 1 && !isView"
type="button"
class="btn-sm role-link"
@click="removeContainer(tab)"
>
{{ t('workload.container.removeContainer') }}
</button>
</template>
<div>
<div
:style="{'align-items':'center'}"
class="row mb-20"
>
<div class="col span-6">
<LabeledInput
v-model:value="allContainers[i].name"
:mode="mode"
:label="t('workload.container.containerName')"
/>
</div>
<div class="col span-6">
<RadioGroup
:mode="mode"
:value="allContainers[i]._init"
name="initContainer"
:options="[true, false]"
:labels="[t('workload.container.init'), t('workload.container.standard')]"
@update:value="updateInitContainer($event, allContainers[i])"
/>
</div>
</div>
<h3>{{ t('workload.container.titles.image') }}</h3>
<div class="row mb-20">
<div class="col span-6">
<LabeledInput
v-model:value.trim="allContainers[i].image"
:mode="mode"
:label="t('workload.container.image')"
:placeholder="t('generic.placeholder', {text: 'nginx:latest'}, true)"
:rules="fvGetAndReportPathRules('image')"
/>
</div>
<div class="col span-6">
<LabeledSelect
v-model:value="allContainers[i].imagePullPolicy"
:label="t('workload.container.imagePullPolicy')"
:options="pullPolicyOptions"
:mode="mode"
/>
</div>
</div>
<div class="row">
<div class="col span-6">
<LabeledSelect
v-model:value="imagePullSecrets"
:label="t('workload.container.imagePullSecrets.label')"
:multiple="true"
:taggable="true"
:options="imagePullNamespacedSecrets"
:mode="mode"
option-label="metadata.name"
:reduce="service=>service.metadata.name"
:create-option="createOption"
:tooltip="t('workload.container.imagePullSecrets.tooltip')"
/>
</div>
</div>
</div>
<div class="spacer" />
<div>
<h3>
{{ t('workload.container.ports.expose') }}
<i
v-clean-tooltip="{content: t('workload.container.ports.toolTip'), triggers: ['hover', 'touch', 'focus'] }"
v-stripped-aria-label="t('workload.container.ports.toolTip')"
class="icon icon-info"
tabindex="0"
role="tooltip"
/>
</h3>
<p class="padded">
{{ t('workload.container.ports.description') }}
</p>
<div class="row">
<WorkloadPorts
v-model:value="allContainers[i].ports"
:name="value.metadata.name"
:services="servicesOwned"
:mode="mode"
/>
</div>
</div>
<div class="spacer" />
<div>
<h3>{{ t('workload.container.titles.command') }}</h3>
<Command
v-model:value="allContainers[i]"
:secrets="namespacedSecrets"
:config-maps="namespacedConfigMaps"
:mode="mode"
:loading="isLoadingSecondaryResources"
/>
</div>
<ServiceNameSelect
:value="podTemplateSpec.serviceAccountName"
:mode="mode"
:select-label="t('workload.serviceAccountName.label')"
:select-placeholder="t('workload.serviceAccountName.label')"
:options="namespacedServiceNames"
option-label="metadata.name"
:loading="isLoadingSecondaryResources"
@update:value="updateServiceAccount"
/>
<div class="spacer" />
<div>
<h3>{{ t('workload.container.titles.lifecycle') }}</h3>
<LifecycleHooks
v-model:value="allContainers[i].lifecycle"
:mode="mode"
/>
</div>
</Tab>
<Tab
:label="t('workload.container.titles.resources')"
name="resources"
:weight="tabWeightMap['resources']"
>
<!-- Resources and Limitations -->
<ContainerResourceLimit
v-model:value="flatResources"
:mode="mode"
:show-tip="false"
/>
</Tab>
<Tab
v-if="!allContainers[i]._init"
:label="t('workload.container.titles.healthCheck')"
name="healthCheck"
:weight="tabWeightMap['healthCheck']"
>
<HealthCheck
:value="allContainers[i]"
:mode="mode"
@update:value="Object.assign(allContainers[i], $event)"
/>
</Tab>
<Tab
:label="t('workload.container.titles.securityContext')"
name="securityContext"
:weight="tabWeightMap['securityContext']"
>
<Security
v-model:value="allContainers[i].securityContext"
:mode="mode"
/>
</Tab>
<Tab
:label="t('workload.storage.title')"
name="storage"
:weight="tabWeightMap['storage']"
>
<ContainerMountPaths
v-model:container="allContainers[i]"
:value="podTemplateSpec"
:namespace="value.metadata.namespace"
:register-before-hook="registerBeforeHook"
:mode="mode"
:secrets="namespacedSecrets"
:config-maps="namespacedConfigMaps"
:save-pvc-hook-name="savePvcHookName"
@removePvcForm="clearPvcFormState"
/>
</Tab>
</Tabbed>
</Tab>
<Tab
v-if="!isPod"
:label="nameDisplayFor(type)"
:name="nameDisplayFor(type)"
:weight="99"
>
<Tabbed
data-testid="workload-general-tabs"
:side-tabs="true"
:use-hash="useTabbedHash"
>
<Tab
name="labels"
label-key="generic.labelsAndAnnotations"
:weight="tabWeightMap['labels']"
>
<Labels
:value="value"
:mode="mode"
@update:value="$emit('input', $event)"
/>
</Tab>
<Tab
:label="t('workload.container.titles.upgrading')"
name="upgrading"
:weight="tabWeightMap['upgrading']"
>
<Job
v-if="isJob || isCronJob"
v-model:value="spec"
:mode="mode"
:type="type"
/>
<Upgrading
v-else
v-model:value="spec"
:mode="mode"
:type="type"
:no-pod-spec="true"
/>
</Tab>
</Tabbed>
</Tab>
<Tab
:label="t('workload.tabs.labels.pod')"
:name="'pod'"
:weight="98"
>
<Tabbed
data-testid="workload-pod-tabs"
:side-tabs="true"
:use-hash="useTabbedHash"
>
<Tab
:label="t('workload.storage.title')"
name="storage-pod"
:weight="tabWeightMap['storage']"
@active="$refs.storage.refresh()"
>
<Storage
ref="storage"
v-model:value="podTemplateSpec"
:namespace="value.metadata.namespace"
:register-before-hook="registerBeforeHook"
:mode="mode"
:secrets="namespacedSecrets"
:config-maps="namespacedConfigMaps"
:save-pvc-hook-name="savePvcHookName"
:loading="isLoadingSecondaryResources"
:namespaced-pvcs="pvcs"
@removePvcForm="clearPvcFormState"
/>
</Tab>
<Tab
:label="t('workload.container.titles.resources')"
name="resources"
:weight="tabWeightMap['resources']"
>
<div>
<div>
<h3 class="mb-10">
<t k="workload.scheduling.titles.tolerations" />
</h3>
<div class="row">
<Tolerations
v-model:value="podTemplateSpec.tolerations"
:mode="mode"
/>
</div>
</div>
<div>
<div class="spacer" />
<h3 class="mb-10">
<t k="workload.scheduling.titles.priority" />
</h3>
<div class="row">
<div class="col span-6">
<LabeledInput
v-model:value.number="podTemplateSpec.priority"
:mode="mode"
:label="t('workload.scheduling.priority.priority')"
/>
</div>
<div class="col span-6">
<LabeledInput
v-model:value="podTemplateSpec.priorityClassName"
:mode="mode"
:label="t('workload.scheduling.priority.className')"
/>
</div>
</div>
</div>
</div>
</Tab>
<Tab
:label="t('workload.container.titles.podScheduling')"
name="podScheduling"
:weight="tabWeightMap['podScheduling']"
>
<PodAffinity
:mode="mode"
:value="podTemplateSpec"
:nodes="allNodeObjects"
:loading="isLoadingSecondaryResources"
/>
</Tab>
<Tab
:label="t('workload.container.titles.nodeScheduling')"
name="nodeScheduling"
:weight="tabWeightMap['nodeScheduling']"
>
<NodeScheduling
:mode="mode"
:value="podTemplateSpec"
:nodes="allNodes"
:loading="isLoadingSecondaryResources"
/>
</Tab>
<Tab
:label="t('workload.container.titles.upgrading')"
name="upgrading"
:weight="tabWeightMap['upgrading']"
>
<Job
v-if="isJob || isCronJob"
v-model:value="spec"
:mode="mode"
:type="type"
/>
<Upgrading
v-else
v-model:value="spec"
:mode="mode"
:type="type"
:no-deployment-spec="true"
/>
</Tab>
<Tab
:label="t('workload.container.titles.securityContext')"
name="securityContext"
:weight="tabWeightMap['securityContext']"
>
<div>
<h3>{{ t('workload.container.security.podFsGroup') }}</h3>
<div class="row">
<div class="col span-6">
<LabeledInput
v-model:value.number="podFsGroup"
type="number"
:mode="mode"
:label="t('workload.container.security.fsGroup')"
/>
</div>
</div>
</div>
</Tab>
<Tab
:label="t('workload.container.titles.networking')"
name="networking"
:weight="tabWeightMap['networking']"
>
<Networking
v-model:value="podTemplateSpec"
:mode="mode"
/>
</Tab>
<Tab
v-if="isStatefulSet"
:label="t('workload.container.titles.volumeClaimTemplates')"
name="volumeClaimTemplates"
:weight="tabWeightMap['volumeClaimTemplates']"
>
<VolumeClaimTemplate
v-model:value="spec"
:mode="mode"
/>
</Tab>
<Tab
name="labels"
label-key="generic.labelsAndAnnotations"
:weight="tabWeightMap['labels']"
>
<div>
<h3>{{ t('workload.container.titles.podLabels') }}</h3>
<div class="row mb-20">
<KeyValue
key="labels"
v-model:value="podLabels"
:add-label="t('labels.addLabel')"
:mode="mode"
:read-allowed="false"
:protip="false"
/>
</div>
<div class="spacer" />
<h3>{{ t('workload.container.titles.podAnnotations') }}</h3>
<div class="row">
<KeyValue
key="annotations"
v-model:value="podAnnotations"
:add-label="t('labels.addAnnotation')"
:mode="mode"
:read-allowed="false"
:protip="false"
/>
</div>
</div>
</Tab>
</Tabbed>
</Tab>
<template #tab-row-extras>
<li class="tablist-controls">
<button
v-if="!isView"
type="button"
class="btn-sm role-link"
data-testid="workload-button-add-container"
@click="addContainerBtn"
>
<i class="icon icon-plus pr-5" /> {{ t('workload.container.addContainer') }}
</button>
</li>
</template>
</Tabbed>
</CruResource>
</form>
</template>
<style lang='scss'>
.container-row {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.type-placeholder {
color: white;
font-size: 2.5em;
height: 100%;
width: 100%;
background-color: var(--primary);
display: flex;
justify-content: center;
align-items: center;
}
.type-description {
color: var(--input-label);
}
.next-dropdown {
display: inline-block;
}
.tab-external-controls {
display: flex;
justify-content: right;
}
.tab-content-controls {
display: flex;
justify-content: right;
}
.tablist-controls {
.role-link {
padding: 10px 15px;
min-height: unset;
line-height: unset;
&:focus {
background: none;
box-shadow: none;
}
&:hover {
border: none;
}
}
}
.deployment-tabs {
> .tabs.horizontal {
border-bottom: 1px solid var(--border);
margin-bottom: 10px;
}
}
.padded {
padding-bottom: 10px;
}
</style>