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:
Westly Wright 2020-10-13 12:04:52 -07:00
parent 55a946fe32
commit aa33b16785
No known key found for this signature in database
GPG Key ID: 4FAB3D8673DC54A3
5 changed files with 163 additions and 75 deletions

View File

@ -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."

View File

@ -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'],
},
];
},

View File

@ -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,
};

View File

@ -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;
}

136
utils/validators/service.js Normal file
View File

@ -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;
}