mirror of https://github.com/rancher/dashboard.git
Service validation
Adds the following validation: Service Ports - adds extra checks for port name, fixes invalid check for externalname services ClusterIP - Conditional Checks on service types and ip none check ExternalName
This commit is contained in:
parent
55a946fe32
commit
aa33b16785
|
|
@ -1235,6 +1235,10 @@ validation:
|
|||
required: '"{key}" is required'
|
||||
requiredOrOverride: '"{key}" is required or must allow override'
|
||||
service:
|
||||
clusterIp:
|
||||
none: 'Service Type is Headless which requires Cluster IP to be set to "None"'
|
||||
externalName:
|
||||
none: 'External Name is required on an ExternalName Service.'
|
||||
ports:
|
||||
name:
|
||||
required: "Port Rule [{position}] - Name is required."
|
||||
|
|
|
|||
|
|
@ -58,11 +58,25 @@ export default {
|
|||
},
|
||||
{
|
||||
nullable: false,
|
||||
path: 'spec.ports',
|
||||
path: 'spec',
|
||||
required: true,
|
||||
type: 'array',
|
||||
validators: ['servicePort'],
|
||||
}
|
||||
},
|
||||
{
|
||||
nullable: true,
|
||||
path: 'spec',
|
||||
required: true,
|
||||
type: 'string',
|
||||
validators: ['clusterIp'],
|
||||
},
|
||||
{
|
||||
nullable: true,
|
||||
path: 'spec',
|
||||
required: true,
|
||||
type: 'array',
|
||||
validators: ['externalName'],
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import { flowOutput } from '@/utils/validators/flow-output';
|
||||
import { servicePort } from '@/utils/validators/service-port';
|
||||
import { clusterIp, externalName, servicePort } from '@/utils/validators/service';
|
||||
|
||||
/**
|
||||
* Custom validation functions beyond normal scalr types
|
||||
* Validator must export a function name should match the validator name on the customValidationRules rule
|
||||
* Exported function is used as a lookup key in resource-instance:validationErrors:customValidationRules loop
|
||||
*/
|
||||
export default { flowOutput, servicePort };
|
||||
export default {
|
||||
clusterIp,
|
||||
externalName,
|
||||
flowOutput,
|
||||
servicePort,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
import isEmpty from 'lodash/isEmpty';
|
||||
import { validateDnsLabel } from '@/utils/validators';
|
||||
|
||||
export function servicePort(ports, getters, errors, validatorArgs) {
|
||||
if (isEmpty(ports)) {
|
||||
errors.push(getters['i18n/t']('validation.required', { key: 'Port Rules' }));
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
ports.forEach((port, ind, ary) => {
|
||||
const {
|
||||
name,
|
||||
nodePort,
|
||||
port: pPort,
|
||||
targetPort,
|
||||
} = port;
|
||||
const idx = ind + 1;
|
||||
|
||||
if (ary.length > 1 && isEmpty(name)) {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.name.required', { position: idx }));
|
||||
}
|
||||
|
||||
if (nodePort) {
|
||||
const np = parseInt(nodePort, 10);
|
||||
|
||||
if (isNaN(np)) {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.nodePort.requiredInt', { position: idx }));
|
||||
}
|
||||
}
|
||||
|
||||
if (pPort) {
|
||||
const p = parseInt(pPort, 10);
|
||||
|
||||
if (isNaN(p)) {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.port.requiredInt', { position: idx }));
|
||||
}
|
||||
} else {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.port.required', { position: idx }));
|
||||
}
|
||||
|
||||
if (targetPort) {
|
||||
const tp = parseInt(targetPort, 10);
|
||||
|
||||
if (isNaN(tp)) {
|
||||
const tpIanaDisplayKey = getters['i18n/t']('validation.service.ports.targetPort.ianaAt', { position: idx });
|
||||
/* [rfc6335](https://tools.ietf.org/rfc/rfc6335.txt) port name (IANA_SVC_NAME)
|
||||
An alphanumeric (a-z, and 0-9) string, with a maximum length of 15 characters,
|
||||
with the '-' character allowed anywhere except the first or the last character or adjacent to another '-' character,
|
||||
it must contain at least a(a - z) character
|
||||
validateChars(str, { validChars: 'A-Za-z0-9_.-' }, displayKey, intl, errors); */
|
||||
const opts = {
|
||||
ianaServiceName: true,
|
||||
maxLength: 15,
|
||||
validChars: 'A-Za-z0-9-',
|
||||
};
|
||||
const isIanaServiceNameErrors = validateDnsLabel(targetPort, tpIanaDisplayKey, getters, opts, errors);
|
||||
|
||||
if (!isEmpty(isIanaServiceNameErrors)) {
|
||||
errors.push(...isIanaServiceNameErrors);
|
||||
}
|
||||
} else if (tp < 1 || tp > 65535) {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.targetPort.between', { position: idx }));
|
||||
}
|
||||
} else {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.targetPort.required', { position: idx }));
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import isEmpty from 'lodash/isEmpty';
|
||||
import { validateDnsLabel, validateHostname } from '@/utils/validators';
|
||||
|
||||
export function servicePort(spec, getters, errors, validatorArgs) {
|
||||
const { ports, type: serviceType } = spec;
|
||||
|
||||
if (serviceType === 'ExternalName') {
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (isEmpty(ports)) {
|
||||
errors.push(getters['i18n/t']('validation.required', { key: 'Port Rules' }));
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
ports.forEach((port, ind, ary) => {
|
||||
const {
|
||||
name,
|
||||
nodePort,
|
||||
port: pPort,
|
||||
targetPort,
|
||||
} = port;
|
||||
const idx = ind + 1;
|
||||
|
||||
if (ary.length > 1 && isEmpty(name)) {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.name.required', { position: idx }));
|
||||
}
|
||||
|
||||
if (!isEmpty(name)) {
|
||||
const nameErrors = validateDnsLabel(name, 'name', getters, undefined, errors);
|
||||
|
||||
if (!isEmpty(nameErrors)) {
|
||||
if (errors.length && errors.length > 0) {
|
||||
errors = [...errors, ...nameErrors];
|
||||
} else {
|
||||
errors = nameErrors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nodePort) {
|
||||
const np = parseInt(nodePort, 10);
|
||||
|
||||
if (isNaN(np)) {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.nodePort.requiredInt', { position: idx }));
|
||||
}
|
||||
}
|
||||
|
||||
if (pPort) {
|
||||
const p = parseInt(pPort, 10);
|
||||
|
||||
if (isNaN(p)) {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.port.requiredInt', { position: idx }));
|
||||
}
|
||||
} else {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.port.required', { position: idx }));
|
||||
}
|
||||
|
||||
if (targetPort) {
|
||||
const tp = parseInt(targetPort, 10);
|
||||
|
||||
if (isNaN(tp)) {
|
||||
const tpIanaDisplayKey = getters['i18n/t']('validation.service.ports.targetPort.ianaAt', { position: idx });
|
||||
/* [rfc6335](https://tools.ietf.org/rfc/rfc6335.txt) port name (IANA_SVC_NAME)
|
||||
An alphanumeric (a-z, and 0-9) string, with a maximum length of 15 characters,
|
||||
with the '-' character allowed anywhere except the first or the last character or adjacent to another '-' character,
|
||||
it must contain at least a(a - z) character
|
||||
validateChars(str, { validChars: 'A-Za-z0-9_.-' }, displayKey, intl, errors); */
|
||||
const opts = {
|
||||
ianaServiceName: true,
|
||||
maxLength: 15,
|
||||
validChars: 'A-Za-z0-9-',
|
||||
};
|
||||
const isIanaServiceNameErrors = validateDnsLabel(targetPort, tpIanaDisplayKey, getters, opts, errors);
|
||||
|
||||
if (!isEmpty(isIanaServiceNameErrors)) {
|
||||
errors.push(...isIanaServiceNameErrors);
|
||||
}
|
||||
} else if (tp < 1 || tp > 65535) {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.targetPort.between', { position: idx }));
|
||||
}
|
||||
} else {
|
||||
errors.push(getters['i18n/t']('validation.service.ports.targetPort.required', { position: idx }));
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function clusterIp(spec, getters, errors, validatorArgs) {
|
||||
/*
|
||||
clusterIP is the IP address of the service and is usually assigned randomly by the master.
|
||||
If an address is specified manually and is not in use by others, it will be allocated to the service; otherwise, creation of the service will fail.
|
||||
This field can not be changed through updates.
|
||||
Valid values are \"None\", empty string (\"\"), or a valid IP address. \"None\" can be specified for headless services when proxying is not required.
|
||||
Only applies to types ClusterIP, NodePort, and LoadBalancer. Ignored if type is ExternalName.
|
||||
More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies
|
||||
*/
|
||||
const typesToCheck = ['ClusterIP', 'NodePort', 'LoadBalancer'];
|
||||
const serviceType = spec?.type;
|
||||
|
||||
if (!typesToCheck.includes(serviceType)) {
|
||||
// validation only applies to services in the types to check
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (serviceType === 'Headless' && spec?.clusterIp !== 'None') {
|
||||
errors.push(getters['i18n/t']('validation.service.clusterIp.none'));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function externalName(spec, getters, errors, validatorArgs) {
|
||||
/*
|
||||
externalName is the external reference that kubedns or equivalent will return as a CNAME record for this service.
|
||||
No proxying will be involved.
|
||||
Must be a valid RFC-1123 hostname (https://tools.ietf.org/html/rfc1123) and requires Type to be ExternalName.
|
||||
*/
|
||||
if (spec?.type !== 'ExternalName' && isEmpty(spec?.externalName)) {
|
||||
errors.push(getters['i18n/t']('validation.service.clusterIp.none'));
|
||||
} else {
|
||||
const hostNameErrors = validateHostname(spec.externalName, 'ExternalName', getters, undefined, errors);
|
||||
|
||||
if (!isEmpty(hostNameErrors)) {
|
||||
if (errors.length && errors.length > 0) {
|
||||
errors = [...errors, ...hostNameErrors];
|
||||
} else {
|
||||
errors = hostNameErrors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
Loading…
Reference in New Issue