mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-05 13:34:10 +00:00
758f3caa48
Change-type: patch
533 lines
11 KiB
TypeScript
533 lines
11 KiB
TypeScript
import * as _ from 'lodash';
|
|
import { inspect } from 'util';
|
|
|
|
import { TargetState } from '../types/state';
|
|
import { EnvVarObject, LabelObject } from './types';
|
|
|
|
import log from './supervisor-console';
|
|
|
|
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\.\-]*$/;
|
|
const NUMERALS_REGEX = /^-?[0-9]+\.?0*$/; // Allows trailing 0 decimals
|
|
|
|
/**
|
|
* checkInt
|
|
*
|
|
* Check an input string as a number, optionally specifying a requirement
|
|
* to be positive
|
|
*/
|
|
export function checkInt(
|
|
s: unknown,
|
|
options: CheckIntOptions = {},
|
|
): number | undefined {
|
|
// Check for non-numeric characters
|
|
if (!NUMERALS_REGEX.test(s as string)) {
|
|
return;
|
|
}
|
|
|
|
const i = Number(s);
|
|
|
|
if (!Number.isInteger(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: unknown): 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: unknown): boolean | undefined {
|
|
if (_.isString(v)) {
|
|
v = v.toLowerCase();
|
|
}
|
|
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: unknown): 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)) {
|
|
log.debug(
|
|
`Non-object passed to validation.isValidEnv\nobj: ${inspect(obj)}`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return _.every(obj, (val, key) => {
|
|
if (!isValidShortText(key)) {
|
|
log.debug(
|
|
`Non-valid short text env var key passed to validation.isValidEnv\nKey: ${inspect(
|
|
key,
|
|
)}`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (!ENV_VAR_KEY_REGEX.test(key)) {
|
|
log.debug(
|
|
`Invalid env var key passed to validation.isValidEnv\nKey: ${inspect(
|
|
key,
|
|
)}`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (!_.isString(val)) {
|
|
log.debug(
|
|
`Non-string value passed to validation.isValidEnv\nValue: ${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)) {
|
|
log.debug(
|
|
`Non-object passed to validation.isValidLabelsObject\nobj: ${inspect(
|
|
obj,
|
|
)}`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return _.every(obj, (val, key) => {
|
|
if (!isValidShortText(key)) {
|
|
log.debug(
|
|
`Non-valid short text label key passed to validation.isValidLabelsObject\nKey: ${inspect(
|
|
key,
|
|
)}`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (!LABEL_NAME_REGEX.test(key)) {
|
|
log.debug(
|
|
`Invalid label name passed to validation.isValidLabelsObject\nKey: ${inspect(
|
|
key,
|
|
)}`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (!_.isString(val)) {
|
|
log.debug(
|
|
`Non-string value passed to validation.isValidLabelsObject\nValue: ${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) {
|
|
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: unknown): boolean {
|
|
if (!_.isObject(apps)) {
|
|
log.debug(
|
|
'Non-object passed to validation.isValidDependentAppsObject\nApps:',
|
|
inspect(apps),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return _.every(apps, (v, appId) => {
|
|
const val: TargetState['dependent']['apps'][any] = _.defaults(_.clone(v), {
|
|
config: undefined,
|
|
environment: undefined,
|
|
commit: undefined,
|
|
image: undefined,
|
|
});
|
|
|
|
if (!isValidShortText(appId) || !checkInt(appId)) {
|
|
log.debug(
|
|
'Invalid appId passed to validation.isValidDependentAppsObject\nappId:',
|
|
inspect(appId),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return _.conformsTo(val, {
|
|
name: (n: any) => {
|
|
if (!isValidShortText(n)) {
|
|
log.debug(
|
|
'Invalid name passed to validation.isValidDependentAppsObject\nName:',
|
|
inspect(n),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
image: (i: any) => {
|
|
if (val.commit != null && !isValidShortText(i)) {
|
|
log.debug(
|
|
'Non valid image passed to validation.isValidDependentAppsObject\nImage:',
|
|
inspect(i),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
commit: (c: any) => {
|
|
if (c != null && !isValidShortText(c)) {
|
|
log.debug(
|
|
'invalid commit passed to validation.isValidDependentAppsObject\nCommit:',
|
|
inspect(c),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
config: (c: any) => {
|
|
if (!undefinedOrValidEnv(c)) {
|
|
log.debug(
|
|
'Invalid config passed to validation.isValidDependentAppsObject\nConfig:',
|
|
inspect(c),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
environment: (e: any) => {
|
|
if (!undefinedOrValidEnv(e)) {
|
|
log.debug(
|
|
'Invalid environment passed to validation.isValidDependentAppsObject\nEnvironment:',
|
|
inspect(e),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
function isValidService(service: any, serviceId: string): boolean {
|
|
if (!isValidShortText(serviceId) || !checkInt(serviceId)) {
|
|
log.debug(
|
|
'Invalid service id passed to validation.isValidService\nService ID:',
|
|
inspect(serviceId),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return _.conformsTo(service, {
|
|
serviceName: (n: any) => {
|
|
if (!isValidShortText(n)) {
|
|
log.debug(
|
|
'Invalid service name passed to validation.isValidService\nService Name:',
|
|
inspect(n),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
image: (i: any) => {
|
|
if (!isValidShortText(i)) {
|
|
log.debug(
|
|
'Invalid image passed to validation.isValidService\nImage:',
|
|
inspect(i),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
environment: (e: any) => {
|
|
if (!isValidEnv(e)) {
|
|
log.debug(
|
|
'Invalid env passed to validation.isValidService\nEnvironment:',
|
|
inspect(e),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
imageId: (i: any) => {
|
|
if (checkInt(i) == null) {
|
|
log.debug(
|
|
'Invalid image id passed to validation.isValidService\nImage ID:',
|
|
inspect(i),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
labels: (l: any) => {
|
|
if (!isValidLabelsObject(l)) {
|
|
log.debug(
|
|
'Invalid labels object passed to validation.isValidService\nLabels:',
|
|
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)) {
|
|
log.debug(
|
|
'Invalid object passed to validation.isValidAppsObject\nobj:',
|
|
inspect(obj),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return _.every(obj, (v, appId) => {
|
|
if (!isValidShortText(appId) || !checkInt(appId)) {
|
|
log.debug(
|
|
'Invalid appId passed to validation.isValidAppsObject\nApp ID:',
|
|
inspect(appId),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// TODO: Remove this partial and validate the extra fields
|
|
const val: Partial<TargetState['local']['apps'][any]> = _.defaults(
|
|
_.clone(v),
|
|
{ releaseId: undefined },
|
|
);
|
|
|
|
return _.conformsTo(val, {
|
|
name: (n: any) => {
|
|
if (!isValidShortText(n)) {
|
|
log.debug(
|
|
'Invalid service name passed to validation.isValidAppsObject\nName:',
|
|
inspect(n),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
releaseId: (r: any) => {
|
|
if (r != null && checkInt(r) == null) {
|
|
log.debug(
|
|
'Invalid releaseId passed to validation.isValidAppsObject\nRelease ID',
|
|
inspect(r),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
services: (s: any) => {
|
|
if (!_.isObject(s)) {
|
|
log.debug(
|
|
'Non-object service passed to validation.isValidAppsObject\nServices:',
|
|
inspect(s),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return _.every(s, (svc, svcId) => {
|
|
if (!isValidService(svc, svcId)) {
|
|
log.debug(
|
|
'Invalid service object passed to validation.isValidAppsObject\nService:',
|
|
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)) {
|
|
log.debug(
|
|
'Non-object passed to validation.isValidDependentDevicesObject\nDevices:',
|
|
inspect(devices),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return _.every(devices, (val, uuid) => {
|
|
if (!isValidShortText(uuid)) {
|
|
log.debug(
|
|
'Invalid uuid passed to validation.isValidDependentDevicesObject\nuuid:',
|
|
inspect(uuid),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return _.conformsTo(val as TargetState['dependent']['devices'][any], {
|
|
name: (n: any) => {
|
|
if (!isValidShortText(n)) {
|
|
log.debug(
|
|
'Invalid device name passed to validation.isValidDependentDevicesObject\nName:',
|
|
inspect(n),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
apps: (a: any) => {
|
|
if (!_.isObject(a)) {
|
|
log.debug(
|
|
'Invalid apps object passed to validation.isValidDependentDevicesObject\nApps:',
|
|
inspect(a),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (_.isEmpty(a)) {
|
|
log.debug(
|
|
'Empty object passed to validation.isValidDependentDevicesObject',
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return _.every(
|
|
a as TargetState['dependent']['devices'][any]['apps'],
|
|
(app) => {
|
|
app = _.defaults(_.clone(app), {
|
|
config: undefined,
|
|
environment: undefined,
|
|
});
|
|
return _.conformsTo(app, {
|
|
config: (c: any) => {
|
|
if (!undefinedOrValidEnv(c)) {
|
|
log.debug(
|
|
'Invalid config passed to validation.isValidDependentDevicesObject\nConfig:',
|
|
inspect(c),
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
environment: (e: any) => {
|
|
if (!undefinedOrValidEnv(e)) {
|
|
log.debug(
|
|
'Invalid environment passed to validation.isValidDependentDevicesObject\nConfig:',
|
|
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);
|
|
}
|