diff --git a/assets/translations/en-us.yaml b/assets/translations/en-us.yaml index acdc3e7ccb2..4c26fb3adac 100644 --- a/assets/translations/en-us.yaml +++ b/assets/translations/en-us.yaml @@ -1306,6 +1306,7 @@ validation: flowOutput: both: Requires "Output" or "Cluster Output" to be selected. global: Requires "Cluster Output" to be selected. + invalidCron: Invalid cron schedule k8s: identifier: emptyLabel: '"{key}" cannot have an empty key' diff --git a/models/workload.js b/models/workload.js index ef3baaa52ca..657ef0e0f8e 100644 --- a/models/workload.js +++ b/models/workload.js @@ -26,6 +26,66 @@ export default { return out; }, + customValidationRules() { + const out = [ + { + nullable: false, + path: 'metadata.name', + required: true, + translationKey: 'generic.name', + type: 'dnsLabel', + }, + { + nullable: false, + path: 'spec', + required: true, + type: 'object', + validators: ['containerImages'], + } + ]; + + const type = this._type ? this._type : this.type; + + switch (type) { + case WORKLOAD_TYPES.DEPLOYMENT: + case WORKLOAD_TYPES.REPLICA_SET: + out.push( { + nullable: false, + path: 'spec.replicas', + required: true, + type: 'number', + translationKey: 'workload.replicas' + }); + break; + case WORKLOAD_TYPES.STATEFUL_SET: + out.push({ + nullable: false, + path: 'spec.replicas', + required: true, + type: 'number', + translationKey: 'workload.replicas' + }); + out.push({ + nullable: false, + path: 'spec.serviceName', + required: true, + type: 'string', + translationKey: 'workload.serviceName' + }); + break; + case WORKLOAD_TYPES.CRON_JOB: + out.push( { + nullable: false, + path: 'spec.schedule', + required: true, + type: 'string', + validators: ['cronSchedule'], + }); + } + + return out; + }, + container() { if (this.type === WORKLOAD_TYPES.CRON_JOB) { // cronjob pod template is nested slightly different than other types diff --git a/plugins/steve/resource-instance.js b/plugins/steve/resource-instance.js index 1ce0a0d7d3e..5833cc41be6 100644 --- a/plugins/steve/resource-instance.js +++ b/plugins/steve/resource-instance.js @@ -1204,6 +1204,7 @@ export default { type: fieldType, } = rule; let pathValue = get(data, path) || null; + const parsedRules = compact((validators || [])); let displayKey = path; diff --git a/utils/custom-validators.js b/utils/custom-validators.js index 9a3d120777b..dafa79a4561 100644 --- a/utils/custom-validators.js +++ b/utils/custom-validators.js @@ -2,6 +2,8 @@ import { flowOutput } from '@/utils/validators/flow-output'; import { clusterIp, externalName, servicePort } from '@/utils/validators/service'; import { ruleGroups, groupsAreValid } from '@/utils/validators/prometheusrule'; import { interval, matching } from '@/utils/validators/monitoring-route'; +import { containerImages } from '@/utils/validators/container-images'; +import { cronSchedule } from '@/utils/validators/cron-schedule'; /** * Custom validation functions beyond normal scalr types @@ -16,5 +18,7 @@ export default { ruleGroups, interval, servicePort, - matching + matching, + containerImages, + cronSchedule }; diff --git a/utils/validators/container-images.js b/utils/validators/container-images.js new file mode 100644 index 00000000000..adfbad50edb --- /dev/null +++ b/utils/validators/container-images.js @@ -0,0 +1,18 @@ +export function containerImages(spec, getters, errors) { + let container; + + if (spec.jobTemplate) { + // cronjob pod template is nested slightly different than other types + const { jobTemplate: { spec: { template: { spec: { containers = [] } } } } } = spec; + + container = containers[0] || {}; + } else { + const { template:{ spec:{ containers = [] } } } = spec; + + container = containers[0] || {}; + } + + if (!container.image) { + errors.push(getters['i18n/t']('validation.required', { key: getters['i18n/t']('workload.container.image') })); + } +} diff --git a/utils/validators/cron-schedule.js b/utils/validators/cron-schedule.js new file mode 100644 index 00000000000..d726f6644c3 --- /dev/null +++ b/utils/validators/cron-schedule.js @@ -0,0 +1,9 @@ +import cronstrue from 'cronstrue'; + +export function cronSchedule(schedule = '', getters, errors) { + try { + cronstrue.toString(schedule); + } catch (e) { + errors.push(getters['i18n/t']('validation.invalidCron')); + } +} diff --git a/utils/validators/index.js b/utils/validators/index.js index da6ddf0bddf..5483dbe844d 100644 --- a/utils/validators/index.js +++ b/utils/validators/index.js @@ -34,10 +34,12 @@ export function validateLength(val, field, displayKey, getters, errors = []) { } = field; const len = val ? get(val, 'length') : 0; - if ( !nullable && required && isEmpty(val) ) { - errors.push(getters['i18n/t']('validation.required', { key: displayKey })); + if ( !nullable && required) { + if ((typeof val === 'object' && isEmpty(val)) || !val) { + errors.push(getters['i18n/t']('validation.required', { key: displayKey })); - return errors; + return errors; + } } if ( val === null ) {