mirror of https://github.com/rancher/dashboard.git
674 lines
21 KiB
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>
|