diff --git a/package.json b/package.json index 3c1c5501c9d..ed17184e40d 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,8 @@ "browser-env": "3.3.0", "cookie": "0.5.0", "cookie-universal-nuxt": "2.1.4", - "cron-validator": "1.2.0", - "cronstrue": "1.95.0", + "cron-validator": "1.3.1", + "cronstrue": "2.50.0", "cross-env": "6.0.3", "d3": "7.3.0", "d3-selection": "1.4.1", diff --git a/pkg/harvester/components/FilterVMSchedule.vue b/pkg/harvester/components/FilterVMSchedule.vue new file mode 100644 index 00000000000..cb4eccb2445 --- /dev/null +++ b/pkg/harvester/components/FilterVMSchedule.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/pkg/harvester/config/harvester.js b/pkg/harvester/config/harvester.js index 3d54eb0c1e8..36f67626455 100644 --- a/pkg/harvester/config/harvester.js +++ b/pkg/harvester/config/harvester.js @@ -418,6 +418,7 @@ export function init($plugin, store) { basicType( [ + HCI.SCHEDULE_VM_BACKUP, HCI.BACKUP, HCI.SNAPSHOT, HCI.VM_SNAPSHOT, @@ -464,6 +465,19 @@ export function init($plugin, store) { exact: false }); + configureType(HCI.SCHEDULE_VM_BACKUP, { showListMasthead: false, showConfigView: false }); + virtualType({ + labelKey: 'harvester.schedule.label', + name: HCI.SCHEDULE_VM_BACKUP, + namespaced: true, + weight: 201, + route: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.SCHEDULE_VM_BACKUP } + }, + exact: false + }); + configureType(HCI.BACKUP, { showListMasthead: false, showConfigView: false }); virtualType({ labelKey: 'harvester.backup.label', diff --git a/pkg/harvester/config/table-headers.js b/pkg/harvester/config/table-headers.js index ecd7afa808c..3d084677892 100644 --- a/pkg/harvester/config/table-headers.js +++ b/pkg/harvester/config/table-headers.js @@ -34,3 +34,39 @@ export const SNAPSHOT_TARGET_VOLUME = { sort: 'spec.source.persistentVolumeClaimName', formatter: 'SnapshotTargetVolume', }; + +// The column of cron expression volume on VM schedules list page +export const VM_SCHEDULE_CRON = { + name: 'CronExpression', + labelKey: 'harvester.tableHeaders.cronExpression', + value: 'spec.cron', + align: 'center', + sort: 'spec.cron', +}; + +// The column of retain on VM schedules list page +export const VM_SCHEDULE_RETAIN = { + name: 'Retain', + labelKey: 'harvester.tableHeaders.retain', + value: 'spec.retain', + sort: 'spec.retain', + align: 'center', +}; + +// The column of maxFailure on VM schedules list page +export const VM_SCHEDULE_MAX_FAILURE = { + name: 'MaxFailure', + labelKey: 'harvester.tableHeaders.maxFailure', + value: 'spec.maxFailure', + sort: 'spec.maxFailure', + align: 'center', +}; + +// The column of type on VM schedules list page +export const VM_SCHEDULE_TYPE = { + name: 'Type', + labelKey: 'harvester.tableHeaders.scheduleType', + value: 'spec.vmbackup.type', + sort: 'spec.vmbackup.type', + align: 'center', +}; diff --git a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue new file mode 100644 index 00000000000..d8ff2abfc83 --- /dev/null +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/BackupList.vue @@ -0,0 +1,129 @@ + + + diff --git a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue new file mode 100644 index 00000000000..99d68f9aa38 --- /dev/null +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/SnapshotList.vue @@ -0,0 +1,97 @@ + + + diff --git a/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue new file mode 100644 index 00000000000..274bcf5eefd --- /dev/null +++ b/pkg/harvester/detail/harvesterhci.io.schedulevmbackup/index.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue b/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue index f97206f2b42..affa26ceaf8 100644 --- a/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue +++ b/pkg/harvester/detail/harvesterhci.io.virtualmachinebackup/index.vue @@ -127,29 +127,29 @@ export default {
-
+
-
+
-
+
-
+
- - - +
+ +
-
+
-
+
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue index 935697d6546..936c46af598 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue @@ -1,9 +1,10 @@ + + diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 0cca30624fa..3c9a7987647 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -52,6 +52,12 @@ harvester: tip: Please enter a template name! success: 'Template { templateName } created successfully.' failed: 'Failed generated template!' + schedule: + title: Create Schedule + message: + tip: Please enter a schedule name! + success: 'Schedule { name } created successfully.' + failed: 'Failed create schedule!' cloneVM: title: Clone VM name: New VM Name @@ -136,8 +142,11 @@ harvester: setDefaultVersion: Set default version addTemplateVersion: Add templateVersion backup: Take Backup + createSchedule: Create VM Schedule restore: Restore restoreNewVM: Restore New + resumeSchedule: Resume + suspendSchedule: Suspend restoreExistingVM: Replace Existing migrate: Migrate abortMigration: Abort Migration @@ -168,6 +177,12 @@ harvester: actions: Actions readyToUse: Ready To Use backupTarget: Backup Target + cronExpression: Cron Expression + retain: Retain + scheduleType: Type + maxFailure: Max Failure + sourceVm: Source VM + vmSchedule: VM Schedule targetVm: Target VM hostIp: Host IP vm: @@ -211,6 +226,7 @@ harvester: promiscuous: Promiscuous ipv4Address: IPv4 Address filterLabels: Filter Labels + filterSchedule: Filter VM Schedule storageClass: Storage Class dockerImage: Docker Image pci: @@ -517,6 +533,7 @@ harvester: size: Size edit: Edit bus: Bus + readyToUse: Ready To Use bootOrder: Boot Order volume: Volume dockerImage: Docker Image @@ -762,6 +779,36 @@ harvester: doc: Before you upgrade to the newer Harvester version, you must perform the required pre-upgrade checks for your cluster. Complete only those tasks that apply to your environment. tip: Failure to perform these checks may result in a failed upgrade or hitting known issues that require a manual workaround fix. moreNotes: For more details about the release notes, please visit - + schedule: + label: VM Schedules + createTitle: Create VM Schedule + createButtonText: Create VM Schedule + scheduleType: VM Schedule Type + cron: Cron Schedule + detail: + namespace: Namespace + sourceVM: Source Virtual Machine + tabs: + basic: Basic + backups: Backups + snapshots: Snapshots + message: + noSetting: + suffix: before creating a backup schedule + retain: + label: Retain + count: Count + tooltip: Number of up-to-date VM backups to retain. Maximum to 250, minimum to 2. + maxFailure: + label: Max Failure + count: Count + tooltip: Max number of consecutive failed backups that could be tolerated. If reach this threshold, Harvester controller will suspend the schedule job. This value should less than retain count + virtualMachine: + title: Virtual Machine Name + placeholder: Select a virtual machine + type: + snapshot: Snapshot + backup: Backup backup: label: VM Backups @@ -800,7 +847,6 @@ harvester: starting: Backup initiating progress: Backup in progress complete: Backup completed - restore: progress: details: Volume details @@ -1414,6 +1460,11 @@ typeLabel: one { Template } other { Templates } } + harvesterhci.io.schedulevmbackup: |- + {count, plural, + one { VM Schedule } + other { VM Schedules } + } harvesterhci.io.virtualmachinebackup: |- {count, plural, one { VM Backup } diff --git a/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue b/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue new file mode 100644 index 00000000000..29b8e30bcd4 --- /dev/null +++ b/pkg/harvester/list/harvesterhci.io.schedulevmbackup.vue @@ -0,0 +1,124 @@ + + + diff --git a/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue b/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue index d563a3252f0..a3d0c2fc06a 100644 --- a/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue +++ b/pkg/harvester/list/harvesterhci.io.virtualmachinebackup.vue @@ -4,36 +4,38 @@ import Loading from '@shell/components/Loading'; import MessageLink from '@shell/components/MessageLink'; import Masthead from '@shell/components/ResourceList/Masthead'; import ResourceTable from '@shell/components/ResourceTable'; - +import FilterVMSchedule from '../components/FilterVMSchedule'; import { HCI } from '../types'; import { allSettled } from '../utils/promise'; import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers'; import { BACKUP_TYPE } from '../config/types'; +import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue'; export default { name: 'HarvesterListBackup', components: { - ResourceTable, Banner, Loading, Masthead, MessageLink + ResourceTable, Banner, Loading, Masthead, MessageLink, FilterVMSchedule }, props: { schema: { type: Object, required: true, - } + }, }, async fetch() { const inStore = this.$store.getters['currentProduct'].inStore; const hash = await allSettled({ - vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }), - settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }), - rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }), + vms: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VM }), + settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }), + backups: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.BACKUP }), + scheduleList: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SCHEDULE_VM_BACKUP }), }); - this.rows = hash.rows; + this.backups = hash.backups; + this.rows = hash.backups; this.settings = hash.settings; - if (this.$store.getters[`${ inStore }/schemaFor`](HCI.SETTING)) { const backupTargetResource = hash.settings.find( O => O.id === 'backup-target'); const isEmpty = this.getBackupTargetValueIsEmpty(backupTargetResource); @@ -50,10 +52,12 @@ export default { const resource = params.resource; return { - rows: [], - settings: [], + rows: [], + backups: [], + settings: [], resource, - to: `${ HCI.SETTING }/backup-target?mode=edit`, + to: `${ HCI.SETTING }/backup-target?mode=edit`, + searchSchedule: '' }; }, @@ -85,7 +89,25 @@ export default { } return out; - } + }, + + getRow(row) { + return row.status && row.status.source; + }, + + changeRows(filteredRows, searchSchedule) { + this.$set(this, 'searchSchedule', searchSchedule); + this.$set(this, 'backups', filteredRows); + }, + + sortGenerationFn() { + let base = defaultTableSortGenerationFn(this.schema, this.$store); + + base += this.searchSchedule; + + return base; + }, + }, computed: { @@ -101,6 +123,12 @@ export default { align: 'left', formatter: 'AttachVMWithName' }, + { + name: 'backupCreatedFrom', + labelKey: 'harvester.tableHeaders.vmSchedule', + value: 'sourceSchedule', + formatter: 'BackupCreatedFrom', + }, { name: 'backupTarget', labelKey: 'tableHeaders.backupTarget', @@ -112,7 +140,7 @@ export default { name: 'readyToUse', labelKey: 'tableHeaders.readyToUse', value: 'status.readyToUse', - align: 'left', + align: 'center', formatter: 'Checked', }, ]; @@ -124,25 +152,24 @@ export default { value: 'backupProgress', align: 'left', formatter: 'HarvesterBackupProgressBar', - width: 200, }); } - cols.push(AGE); return cols; }, hasBackupProgresses() { - return !!this.rows.find(R => R.status?.progress !== undefined); + return !!this.backups.find(r => r.status?.progress !== undefined); }, - filteredRows() { - return this.rows.filter(R => R.spec?.type !== BACKUP_TYPE.SNAPSHOT); + return this.backups.filter(r => r.spec?.type !== BACKUP_TYPE.SNAPSHOT); + }, + getRawRows() { + return this.rows.filter(r => r.spec?.type === BACKUP_TYPE.BACKUP); }, - backupTargetResource() { - return this.settings.find( O => O.id === 'backup-target'); + return this.settings.find(O => O.id === 'backup-target'); }, isEmptyValue() { @@ -211,16 +238,23 @@ export default { :headers="headers" :groupable="true" :rows="filteredRows" + :sort-generation-fn="sortGenerationFn" :schema="schema" key-field="_key" default-sort-by="age" v-on="$listeners" > + diff --git a/pkg/harvester/mixins/harvester-vm/index.js b/pkg/harvester/mixins/harvester-vm/index.js index ea31a5a3213..f34f143483f 100644 --- a/pkg/harvester/mixins/harvester-vm/index.js +++ b/pkg/harvester/mixins/harvester-vm/index.js @@ -302,7 +302,7 @@ export default { } = config; const vm = this.resource === HCI.VM ? value : this.resource === HCI.BACKUP ? this.value.status?.source : value.spec.vm; - + const volumeBackups = this.resource === HCI.BACKUP ? this.value.status?.volumeBackups : null; const spec = vm?.spec; if (!spec) { @@ -336,7 +336,8 @@ export default { const sshKey = this.getSSHFromAnnotation(spec) || []; const imageId = this.getRootImageId(vm) || ''; - const diskRows = this.getDiskRows(vm); + const diskRows = this.getDiskRows(vm, volumeBackups); + const networkRows = this.getNetworkRows(vm, { fromTemplate, init }); const hasCreateVolumes = this.getHasCreatedVolumes(spec) || []; @@ -403,7 +404,8 @@ export default { this.refreshYamlEditor(); }, - getDiskRows(vm) { + getDiskRows(vm, volumeBackups) { + console.log('🚀 ~ getDiskRows ~ volumeBackups:', volumeBackups); const namespace = vm.metadata.namespace; const _volumes = vm.spec.template.spec.volumes || []; const _disks = vm.spec.template.spec.domain.devices.disks || []; @@ -424,6 +426,7 @@ export default { storageClassName: '', image: this.imageId, volumeMode: 'Block', + volumeBackups: volumeBackups?.find(vBackup => vBackup.volumeName === 'disk-0') || null, }); } else { out = _disks.map( (DISK, index) => { @@ -503,24 +506,25 @@ export default { const volumeStatus = this.pvcs.find(P => P.id === `${ this.value.metadata.namespace }/${ volumeName }`)?.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR]; return { - id: randomStr(5), + id: randomStr(5), bootOrder, source, - name: DISK.name, + name: DISK.name, realName, bus, volumeName, container, accessMode, - size: `${ formatSize }Gi`, - volumeMode: volumeMode || this.customVolumeMode, + size: `${ formatSize }Gi`, + volumeMode: volumeMode || this.customVolumeMode, image, type, storageClassName, hotpluggable, volumeStatus, dataSource, - namespace + namespace, + volumeBackups: volumeBackups?.find(vBackup => vBackup.volumeName === DISK.name) || null, }; }); } diff --git a/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js b/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js new file mode 100644 index 00000000000..13a2f566100 --- /dev/null +++ b/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js @@ -0,0 +1,97 @@ +import HarvesterResource from './harvester'; +import { get } from '@shell/utils/object'; +import { findBy } from '@shell/utils/array'; +import { colorForState, stateDisplay, STATES } from '@shell/plugins/dashboard-store/resource-class'; +import { _CREATE } from '@shell/config/query-params'; +import { ucFirst, escapeHtml } from '@shell/utils/string'; + +export default class ScheduleVmBackup extends HarvesterResource { + detailPageHeaderActionOverride(realMode) { + if (realMode === _CREATE) { + return this.t('harvester.schedule.createTitle'); + } + } + + get _availableActions() { + const toFilter = ['goToClone']; + + const out = super._availableActions.filter((action) => { + if (!toFilter.includes(action.action)) { + return action; + } + }); + + return [ + { + action: 'resumeSchedule', + enabled: ucFirst(this.state) === STATES.suspended.label, + icon: 'icons icon-play', + label: this.t('harvester.action.resumeSchedule'), + }, + { + action: 'suspendSchedule', + enabled: ucFirst(this.state) === STATES.active.label, + icon: 'icons icon-pause', + label: this.t('harvester.action.suspendSchedule'), + }, + ...out + ]; + } + + async suspendSchedule() { + try { + this.spec.suspend = true; // suspend schedule + await this.save(); + } catch (err) { + this.spec.suspend = false; + + this.$dispatch('growl/fromError', { + title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }), + err, + }, { root: true }); + } + } + + async resumeSchedule() { + try { + this.spec.suspend = false; // resume schedule + await this.save(); + } catch (err) { + this.spec.suspend = true; + + this.$dispatch('growl/fromError', { + title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }), + err, + }, { root: true }); + } + } + + get state() { + const conditions = get(this, 'status.conditions'); + const isSuspended = findBy(conditions, 'type', 'BackupSuspend')?.status === 'True'; + + if (isSuspended) { + return STATES.suspended.label; + } + + return this.metadata.state.name; + } + + get stateDescription() { + const suspendedCondition = (this.status?.conditions || []).find(c => c.type === 'BackupSuspend'); + + return ucFirst(suspendedCondition?.message) || super.stateDescription; + } + + get stateBackground() { + return colorForState(this.stateDisplay).replace('text-', 'bg-'); + } + + get stateColor() { + return colorForState(this.state); + } + + get stateDisplay() { + return stateDisplay(this.state); + } +} diff --git a/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js b/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js index 8857f1352e9..fa9a2220c28 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachinebackup.js @@ -18,9 +18,8 @@ export default class HciVmBackup extends HarvesterResource { get detailLocation() { const detailLocation = clone(this._detailLocation); - const route = this.currentRoute(); - detailLocation.params.resource = route.params.resource; + detailLocation.params.resource = HCI.BACKUP; return detailLocation; } @@ -81,23 +80,22 @@ export default class HciVmBackup extends HarvesterResource { } restoreExistingVM(resource = this) { - const route = this.currentRoute(); const router = this.currentRouter(); router.push({ name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, - params: { resource: route.params.resource }, + params: { resource: HCI.BACKUP }, query: { restoreMode: 'existing', resourceName: resource.name } }); } restoreNewVM(resource = this) { - const route = this.currentRoute(); + // const route = this.currentRoute(); const router = this.currentRouter(); router.push({ name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, - params: { resource: route.params.resource }, + params: { resource: HCI.BACKUP }, query: { restoreMode: 'new', resourceName: resource.name } }); } @@ -125,6 +123,10 @@ export default class HciVmBackup extends HarvesterResource { return colorForState(state); } + get sourceSchedule() { + return this.metadata?.annotations['harvesterhci.io/svmbackupId']; + } + get attachVM() { return this.spec.source.name; } diff --git a/pkg/harvester/models/kubevirt.io.virtualmachine.js b/pkg/harvester/models/kubevirt.io.virtualmachine.js index e4cd26f8d2e..a8822992fb5 100644 --- a/pkg/harvester/models/kubevirt.io.virtualmachine.js +++ b/pkg/harvester/models/kubevirt.io.virtualmachine.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import { load } from 'js-yaml'; import { omitBy, pickBy } from 'lodash'; - +import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester'; import { colorForState } from '@shell/plugins/dashboard-store/resource-class'; import { POD, NODE, PVC } from '@shell/config/types'; import { HCI } from '../types'; @@ -156,6 +156,12 @@ export default class VirtVm extends HarvesterResource { icon: 'icon icon-backup', label: this.t('harvester.action.vmSnapshot') }, + { + action: 'createSchedule', + enabled: true, + icon: 'icon icon-history', + label: this.t('harvester.action.createSchedule') + }, { action: 'restoreVM', enabled: !!this.actions?.restore, @@ -318,6 +324,16 @@ export default class VirtVm extends HarvesterResource { ); } + createSchedule(resources = this) { + const router = this.currentRouter(); + + router.push({ + name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, + params: { resource: HCI.SCHEDULE_VM_BACKUP }, + query: { vmNamespace: this.metadata.namespace, vmName: this.metadata.name } + }); + } + backupVM(resources = this) { this.$dispatch('promptModal', { resources, diff --git a/pkg/harvester/types.ts b/pkg/harvester/types.ts index 6638cacc77d..5dad3b1d5b4 100644 --- a/pkg/harvester/types.ts +++ b/pkg/harvester/types.ts @@ -11,6 +11,7 @@ export const HCI = { SETTING: 'harvesterhci.io.setting', UPGRADE: 'harvesterhci.io.upgrade', UPGRADE_LOG: 'harvesterhci.io.upgradelog', + SCHEDULE_VM_BACKUP: 'harvesterhci.io.schedulevmbackup', BACKUP: 'harvesterhci.io.virtualmachinebackup', RESTORE: 'harvesterhci.io.virtualmachinerestore', NODE_NETWORK: 'network.harvesterhci.io.nodenetwork', diff --git a/pkg/rancher-components/package.json b/pkg/rancher-components/package.json index d31e73d2955..ed7767b5615 100644 --- a/pkg/rancher-components/package.json +++ b/pkg/rancher-components/package.json @@ -32,8 +32,8 @@ "@vue/test-utils": "1.2.1", "babel-eslint": "10.1.0", "core-js": "3.25.3", - "cron-validator": "1.2.0", - "cronstrue": "1.95.0", + "cron-validator": "1.3.1", + "cronstrue": "2.50.0", "eslint": "7.32.0", "eslint-plugin-import": "2.23.4", "eslint-plugin-node": "11.1.0", diff --git a/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue b/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue index c4bd59177ac..109d19bb6fd 100644 --- a/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue +++ b/pkg/rancher-components/src/components/Form/LabeledInput/LabeledInput.vue @@ -144,11 +144,16 @@ export default ( if (this.type !== 'cron' || !this.value) { return; } - if (!isValidCron(this.value)) { + // refer https://github.com/GuillaumeRochat/cron-validator#readme + if (!isValidCron(this.value, { + alias: true, + allowBlankDay: true, + allowSevenAsSunday: true, + })) { return this.t('generic.invalidCron'); } try { - const hint = cronstrue.toString(this.value); + const hint = cronstrue.toString(this.value, { verbose: true }); return hint; } catch (e) { diff --git a/shell/package.json b/shell/package.json index 5744afeaab1..d6e6ea9740b 100644 --- a/shell/package.json +++ b/shell/package.json @@ -58,8 +58,8 @@ "cookie": "0.5.0", "cookie-universal-nuxt": "2.1.4", "core-js": "3.21.1", - "cron-validator": "1.2.0", - "cronstrue": "1.95.0", + "cron-validator": "1.3.1", + "cronstrue": "2.50.0", "cross-env": "6.0.3", "css-loader": "6.7.3", "csv-loader": "3.0.3", diff --git a/shell/utils/validators/cron-schedule.js b/shell/utils/validators/cron-schedule.js index d726f6644c3..ebea8885c5f 100644 --- a/shell/utils/validators/cron-schedule.js +++ b/shell/utils/validators/cron-schedule.js @@ -7,3 +7,13 @@ export function cronSchedule(schedule = '', getters, errors) { errors.push(getters['i18n/t']('validation.invalidCron')); } } + +export function isCronValid(schedule = '') { + try { + const hint = cronstrue.toString(schedule); + + return !!hint; + } catch (e) { + return false; + } +} diff --git a/yarn.lock b/yarn.lock index 2d93df2cc7c..c7225b406cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5490,10 +5490,15 @@ cron-validator@1.2.0: resolved "https://registry.yarnpkg.com/cron-validator/-/cron-validator-1.2.0.tgz#952d2c926b85724dfe9c0d0ca781fe956124de93" integrity sha512-fX9eq71ToAt4bJeJzFNe8OCljKNQdc2Otw4kZDfB3vyplrAyEO9Q20YgmCJ4pr+jI/QQ2yizM87Eh+b2Ty7GuQ== -cronstrue@1.95.0: - version "1.95.0" - resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.95.0.tgz#171df1fad8b0f0cb636354dd1d7842161c15478f" - integrity sha512-CdbQ17Z8Na2IdrK1SiD3zmXfE66KerQZ8/iApkGsxjmUVGJPS9M9oK4FZC3LM6ohUjjq3UeaSk+90Cf3QbXDfw== +cron-validator@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/cron-validator/-/cron-validator-1.3.1.tgz#8f2fe430f92140df77f91178ae31fc1e3a48a20e" + integrity sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A== + +cronstrue@2.50.0: + version "2.50.0" + resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.50.0.tgz#eabba0f915f186765258b707b7a3950c663b5573" + integrity sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg== cross-env@6.0.3: version "6.0.3"