balena-supervisor/src/lib/validation.ts
Cameron Diver ae446c01b2
ux: Warn on invalid device name when trying to start a service
Device names with newlines cause reboot loops, due to newlines not being
supported by docker. This PR will warn when a device name contains a
newline.

Change-type: patch
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-08-16 21:52:49 +01:00

438 lines
11 KiB
TypeScript

import * as _ from 'lodash';
import { inspect } from 'util';
import { EnvVarObject, LabelObject } from './types';
export interface CheckIntOptions {
positive?: boolean;
}
const ENV_VAR_KEY_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
const LABEL_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9\.\-]*$/;
type NullableString = string | undefined | null;
/**
* checkInt
*
* Check an input string as a number, optionally specifying a requirement
* to be positive
*/
export function checkInt(s: NullableString, options: CheckIntOptions = {}): number | void {
if (s == null) {
return;
}
const i = parseInt(s, 10);
if (isNaN(i)) {
return;
}
if (options.positive && i <= 0) {
return;
}
return i;
}
/**
* checkString
*
* Check that a string exists, and is not an empty string, 'null', or 'undefined'
*/
export function checkString(s: NullableString): string | void {
if (s == null || !_.isString(s) || _.includes([ 'null', 'undefined', '' ], s)) {
return;
}
return s;
}
/**
* checkTruthy
*
* Given a value which can be a string, boolean or number, return a boolean
* which represents if the input was truthy
*/
export function checkTruthy(v: string | boolean | number): boolean | void {
switch(v) {
case '1':
case 'true':
case true:
case 'on':
case 1:
return true;
case '0':
case 'false':
case false:
case 'off':
case 0:
return false;
default:
return;
}
}
/*
* isValidShortText
*
* Check that the input string is definitely a string,
* and has a length which is less than 255
*/
export function isValidShortText(t: string): boolean {
return _.isString(t) && t.length <= 255;
}
/**
* isValidEnv
*
* Given a env var object, check types and values for the keys
* and values
*/
export function isValidEnv(obj: EnvVarObject): boolean {
if (!_.isObject(obj)) {
console.log('debug: Non-object passed to validation.isValidEnv');
console.log(`\tobj: ${inspect(obj)}`);
return false;
}
return _.every(obj, (val, key) => {
if (!isValidShortText(key)) {
console.log('debug: Non-valid short text env var key passed to validation.isValidEnv');
console.log(`\tKey: ${inspect(key)}`);
return false;
}
if (!ENV_VAR_KEY_REGEX.test(key)) {
console.log('debug: Invalid env var key passed to validation.isValidEnv');
console.log(`\tKey: ${inspect(key)}`);
return false;
}
if (!_.isString(val)) {
console.log('debug: Non-string value passed to validation.isValidEnv');
console.log(`\tval: ${inspect(key)}`);
return false;
}
return true;
});
}
/**
* isValidLabelsObject
*
* Given a labels object, test the types and values for validity
*/
export function isValidLabelsObject(obj: LabelObject): boolean {
if (!_.isObject(obj)) {
console.log('debug: Non-object passed to validation.isValidLabelsObject');
console.log(`\tobj: ${inspect(obj)}`);
return false;
}
return _.every(obj, (val, key) => {
if (!isValidShortText(key)) {
console.log('debug: Non-valid short text label key passed to validation.isValidLabelsObject');
console.log(`\tkey: ${inspect(key)}`);
return false;
}
if (!LABEL_NAME_REGEX.test(key)) {
console.log('debug: Invalid label name passed to validation.isValidLabelsObject');
console.log(`\tkey: ${inspect(key)}`);
return false;
}
if (!_.isString(val)) {
console.log('debug: Non-string value passed to validation.isValidLabelsObject');
console.log(`\tval: ${inspect(val)}`);
return false;
}
return true;
});
}
export function isValidDeviceName(name: string): boolean {
// currently the only disallowed value in a device name is a newline
const newline = name.indexOf('\n') !== -1;
if (newline) {
console.log('debug: newline found in device name. This is invalid and should be removed');
}
return !newline;
}
function undefinedOrValidEnv(val: EnvVarObject): boolean {
return val == null || isValidEnv(val);
}
/**
* isValidDependentAppsObject
*
* Given a dependent apps object from a state endpoint, validate it
*
* TODO: Type the input
*/
export function isValidDependentAppsObject(apps: any): boolean {
if (!_.isObject(apps)) {
console.log('debug: non-object passed to validation.isValidDependentAppsObject');
console.log(`\tapps: ${inspect(apps)}`);
return false;
}
return _.every(apps, (val, appId) => {
val = _.defaults(_.clone(val), {
config: undefined,
environment: undefined,
commit: undefined,
image: undefined,
});
if (!isValidShortText(appId) || !checkInt(appId)) {
console.log('debug: Invalid appId passed to validation.isValidDependentAppsObject');
console.log(`\tappId: ${inspect(appId)}`);
return false;
}
return _.conformsTo(val, {
name: (n: any) => {
if (!isValidShortText(n)) {
console.log('debug: Invalid name passed to validation.isValidDependentAppsObject');
console.log(`\tname: ${inspect(n)}`);
return false;
}
return true;
},
image: (i: any) => {
if (val.commit != null && !isValidShortText(i)) {
console.log('debug: non valid image passed to validation.isValidDependentAppsObject');
console.log(`\timage: ${inspect(i)}`);
return false;
}
return true;
},
commit: (c: any) => {
if (c != null && !isValidShortText(c)) {
console.log('debug: invalid commit passed to validation.isValidDependentAppsObject');
console.log(`\tcommit: ${inspect(c)}`);
return false;
}
return true;
},
config: (c: any) => {
if (!undefinedOrValidEnv(c)) {
console.log('debug; Invalid config passed to validation.isValidDependentAppsObject');
console.log(`\tconfig: ${inspect(c)}`);
return false;
}
return true;
},
environment: (e: any) => {
if (!undefinedOrValidEnv(e)) {
console.log('debug; Invalid environment passed to validation.isValidDependentAppsObject');
console.log(`\tenvironment: ${inspect(e)}`);
return false;
}
return true;
},
});
});
}
function isValidService(service: any, serviceId: string): boolean {
if (!isValidShortText(serviceId) || !checkInt(serviceId)) {
console.log('debug: Invalid service id passed to validation.isValidService');
console.log(`\tserviceId: ${inspect(serviceId)}`);
return false;
}
return _.conformsTo(service, {
serviceName: (n: any) => {
if (!isValidShortText(n)) {
console.log('debug: Invalid service name passed to validation.isValidService');
console.log(`\tserviceName: ${inspect(n)}`);
return false;
}
return true;
},
image: (i: any) => {
if (!isValidShortText(i)) {
console.log('debug: Invalid image passed to validation.isValidService');
console.log(`\timage: ${inspect(i)}`);
return false;
}
return true;
},
environment: (e: any) => {
if (!isValidEnv(e)) {
console.log('debug: Invalid env passed to validation.isValidService');
console.log(`\tenvironment: ${inspect(e)}`);
return false;
}
return true;
},
imageId: (i: any) => {
if (checkInt(i) == null) {
console.log('debug: Invalid image id passed to validation.isValidService');
console.log(`\timageId: ${inspect(i)}`);
return false;
}
return true;
},
labels: (l: any) => {
if (!isValidLabelsObject(l)) {
console.log('debug: Invalid labels object passed to validation.isValidService');
console.log(`\tlabels: ${inspect(l)}`);
return false;
}
return true;
},
});
}
/**
* isValidAppsObject
*
* Given an apps object from the state endpoint, validate the fields and
* return whether it's valid.
*
* TODO: Type the input correctly
*/
export function isValidAppsObject(obj: any): boolean {
if (!_.isObject(obj)) {
console.log('debug: Invalid object passed to validation.isValidAppsObject');
console.log(`\tobj: ${inspect(obj)}`);
return false;
}
return _.every(obj, (val, appId) => {
if (!isValidShortText(appId) || !checkInt(appId)) {
console.log('debug: Invalid appId passed to validation.isValidAppsObject');
console.log(`\tappId: ${inspect(appId)}`);
return false;
}
return _.conformsTo(_.defaults(_.clone(val), { releaseId: undefined }), {
name: (n: any) => {
if (!isValidShortText(n)) {
console.log('debug: Invalid service name passed to validation.isValidAppsObject');
console.log(`\tname: ${inspect(n)}`);
return false;
}
return true;
},
releaseId: (r: any) => {
if (r != null && checkInt(r) == null) {
console.log('debug: Invalid releaseId passed to validation.isValidAppsObject');
console.log(`\treleaseId: ${inspect(r)}`);
return false;
}
return true;
},
services: (s: any) => {
if (!_.isObject(s)) {
console.log('debug: Non-object service passed to validation.isValidAppsObject');
console.log(`\tservices: ${inspect(s)}`);
return false;
}
return _.every(s, (svc, svcId) => {
if (!isValidService(svc, svcId)) {
console.log('debug: Invalid service object passed to validation.isValidAppsObject');
console.log(`\tsvc: ${inspect(svc)}`);
return false;
}
return true;
});
},
});
});
}
/**
* isValidDependentDevicesObject
*
* Validate a dependent devices object from the state endpoint.
*/
export function isValidDependentDevicesObject(devices: any): boolean {
if (!_.isObject(devices)) {
console.log('debug: Non-object passed to validation.isValidDependentDevicesObject');
console.log(`\tdevices: ${inspect(devices)}`);
return false;
}
return _.every(devices, (val, uuid) => {
if (!isValidShortText(uuid)) {
console.log('debug: Invalid uuid passed to validation.isValidDependentDevicesObject');
console.log(`\tuuid: ${inspect(uuid)}`);
return false;
}
return _.conformsTo(val, {
name: (n: any) => {
if (!isValidShortText(n)) {
console.log('debug: Invalid device name passed to validation.isValidDependentDevicesObject');
console.log(`\tname: ${inspect(n)}`);
return false;
}
return true;
},
apps: (a: any) => {
if (!_.isObject(a)) {
console.log('debug: Invalid apps object passed to validation.isValidDependentDevicesObject');
console.log(`\tapps: ${inspect(a)}`);
return false;
}
if (_.isEmpty(a)) {
console.log('debug: Empty object passed to validation.isValidDependentDevicesObject');
return false;
}
return _.every(a, (app) => {
app = _.defaults(_.clone(app), { config: undefined, environment: undefined });
return _.conformsTo(app, {
config: (c: any) => {
if (!undefinedOrValidEnv(c)) {
console.log('debug: Invalid config passed to validation.isValidDependentDevicesObject');
console.log(`\tconfig: ${inspect(c)}`);
return false;
}
return true;
},
environment: (e: any) => {
if (!undefinedOrValidEnv(e)) {
console.log('debug: Invalid environment passed to validation.isValidDependentDevicesObject');
console.log(`\tconfig: ${inspect(e)}`);
return false;
}
return true;
},
});
});
},
});
});
}
/**
* validStringOrUndefined
*
* Ensure a string is either undefined, or a non-empty string
*/
export function validStringOrUndefined(s: string | undefined): boolean {
return _.isUndefined(s) || (_.isString(s) && !_.isEmpty(s));
}
/**
* validStringOrUndefined
*
* Ensure an object is either undefined or an actual object
*/
export function validObjectOrUndefined(o: object | undefined): boolean {
return _.isUndefined(o) || _.isObject(o);
}