mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-04-10 12:49:54 +00:00
Start initial typescript conversion, and add validation debugging
Add webpack config and dependencies to have typescript built, and also convert src/lib/validation.coffee to typescript. In this conversion I also added a lot of debugging which should help the upcoming local mode development. Change-type: minor Signed-off-by: Cameron Diver <cameron@resin.io>
This commit is contained in:
parent
78107b1199
commit
cfddbf65e4
@ -38,7 +38,7 @@ COPY package.json /usr/src/app/
|
||||
|
||||
RUN JOBS=MAX npm install --no-optional --unsafe-perm
|
||||
|
||||
COPY webpack.config.js fix-jsonstream.js hardcode-migrations.js /usr/src/app/
|
||||
COPY webpack.config.js fix-jsonstream.js hardcode-migrations.js tsconfig.json /usr/src/app/
|
||||
COPY src /usr/src/app/src
|
||||
COPY test /usr/src/app/test
|
||||
|
||||
|
@ -12,9 +12,10 @@
|
||||
"build": "webpack",
|
||||
"lint": "resin-lint src/ test/",
|
||||
"versionist": "versionist",
|
||||
"test": "npm run lint && mocha -r coffee-script/register test/**/*"
|
||||
"test": "npm run lint && mocha -r ts-node/register -r coffee-script/register test/**/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/lodash": "^4.14.108",
|
||||
"mkfifo": "^0.1.5",
|
||||
"sqlite3": "^3.1.0"
|
||||
},
|
||||
@ -47,6 +48,7 @@
|
||||
"mixpanel": "0.0.20",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mocha": "^5.0.5",
|
||||
"mochainon": "^2.0.0",
|
||||
"network-checker": "~0.0.5",
|
||||
"node-loader": "^0.6.0",
|
||||
"null-loader": "^0.1.1",
|
||||
@ -61,7 +63,10 @@
|
||||
"semver": "^5.3.0",
|
||||
"semver-regex": "^1.0.0",
|
||||
"shell-quote": "^1.6.1",
|
||||
"ts-loader": "^3.5.0",
|
||||
"ts-node": "^6.0.1",
|
||||
"typed-error": "~0.1.0",
|
||||
"typescript": "^2.8.3",
|
||||
"uglifyjs-webpack-plugin": "^1.0.1",
|
||||
"versionist": "^2.8.0",
|
||||
"webpack": "^3.0.0"
|
||||
|
@ -1,94 +0,0 @@
|
||||
_ = require 'lodash'
|
||||
|
||||
exports.checkInt = checkInt = (s, options = {}) ->
|
||||
# Make sure `s` exists and is not an empty string.
|
||||
if !s?
|
||||
return
|
||||
i = parseInt(s, 10)
|
||||
if isNaN(i)
|
||||
return
|
||||
if options.positive && i <= 0
|
||||
return
|
||||
return i
|
||||
|
||||
exports.checkString = (s) ->
|
||||
# Make sure `s` exists and is not an empty string, or 'null' or 'undefined'.
|
||||
# This might happen if the parsing of config.json on the host using jq is wrong (it is buggy in some versions).
|
||||
if !s? or !_.isString(s) or s in [ 'null', 'undefined', '' ]
|
||||
return
|
||||
return s
|
||||
|
||||
exports.checkTruthy = (v) ->
|
||||
switch v
|
||||
when '1', 'true', true, 'on', 1 then true
|
||||
when '0', 'false', false, 'off', 0 then false
|
||||
else return
|
||||
|
||||
exports.isValidShortText = isValidShortText = (t) ->
|
||||
_.isString(t) and t.length <= 255
|
||||
exports.isValidEnv = isValidEnv = (obj) ->
|
||||
_.isObject(obj) and _.every obj, (val, key) ->
|
||||
isValidShortText(key) and /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key) and _.isString(val)
|
||||
|
||||
exports.isValidLabelsObject = isValidLabelsObject = (obj) ->
|
||||
_.isObject(obj) and _.every obj, (val, key) ->
|
||||
isValidShortText(key) and /^[a-zA-Z][a-zA-Z0-9\.\-]*$/.test(key) and _.isString(val)
|
||||
|
||||
undefinedOrValidEnv = (val) ->
|
||||
if val? and !isValidEnv(val)
|
||||
return false
|
||||
return true
|
||||
|
||||
exports.isValidDependentAppsObject = (apps) ->
|
||||
return false if !_.isObject(apps)
|
||||
return _.every apps, (val, appId) ->
|
||||
val = _.defaults(_.clone(val), { config: undefined, environment: undefined, commit: undefined, image: undefined })
|
||||
return false if !isValidShortText(appId) or !checkInt(appId)?
|
||||
return _.conformsTo(val, {
|
||||
name: isValidShortText
|
||||
image: (i) -> !val.commit? or isValidShortText(i)
|
||||
commit: (c) -> !c? or isValidShortText(c)
|
||||
config: undefinedOrValidEnv
|
||||
environment: undefinedOrValidEnv
|
||||
})
|
||||
|
||||
isValidService = (service, serviceId) ->
|
||||
return false if !isValidShortText(serviceId) or !checkInt(serviceId)
|
||||
return _.conformsTo(service, {
|
||||
serviceName: isValidShortText
|
||||
image: isValidShortText
|
||||
environment: isValidEnv
|
||||
imageId: (i) -> checkInt(i)?
|
||||
labels: isValidLabelsObject
|
||||
})
|
||||
|
||||
exports.isValidAppsObject = (obj) ->
|
||||
return false if !_.isObject(obj)
|
||||
return _.every obj, (val, appId) ->
|
||||
return false if !isValidShortText(appId) or !checkInt(appId)?
|
||||
return _.conformsTo(_.defaults(_.clone(val), { releaseId: undefined }), {
|
||||
name: isValidShortText
|
||||
releaseId: (r) -> !r? or checkInt(r)?
|
||||
services: (s) -> _.isObject(s) and _.every(s, isValidService)
|
||||
})
|
||||
|
||||
exports.isValidDependentDevicesObject = (devices) ->
|
||||
return false if !_.isObject(devices)
|
||||
return _.every devices, (val, uuid) ->
|
||||
return false if !isValidShortText(uuid)
|
||||
return _.conformsTo(val, {
|
||||
name: isValidShortText
|
||||
apps: (a) ->
|
||||
return (
|
||||
_.isObject(a) and
|
||||
!_.isEmpty(a) and
|
||||
_.every a, (app) ->
|
||||
app = _.defaults(_.clone(app), { config: undefined, environment: undefined })
|
||||
_.conformsTo(app, { config: undefinedOrValidEnv, environment: undefinedOrValidEnv })
|
||||
)
|
||||
})
|
||||
|
||||
exports.validStringOrUndefined = (s) ->
|
||||
_.isUndefined(s) or (_.isString(s) and !_.isEmpty(s))
|
||||
exports.validObjectOrUndefined = (o) ->
|
||||
_.isUndefined(o) or _.isObject(o)
|
432
src/lib/validation.ts
Normal file
432
src/lib/validation.ts
Normal file
@ -0,0 +1,432 @@
|
||||
import * as _ from 'lodash';
|
||||
import { inspect } from 'util';
|
||||
|
||||
export interface CheckIntOptions {
|
||||
positive?: boolean;
|
||||
}
|
||||
|
||||
export interface EnvVarObject {
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
export interface LabelObject {
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
const ENV_VAR_KEY_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
const LABEL_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9\.\-]*$/;
|
||||
|
||||
/**
|
||||
* checkInt
|
||||
*
|
||||
* Check an input string as a number, optionally specifying a requirement
|
||||
* to be positive
|
||||
*/
|
||||
export function checkInt(s: string | null, 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: string): 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;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": true,
|
||||
"preserveConstEnums": true,
|
||||
"removeComments": true,
|
||||
"sourceMap": true,
|
||||
"strictNullChecks": true,
|
||||
"outDir": "./build"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"typings/**/*.d.ts"
|
||||
]
|
||||
}
|
@ -77,7 +77,7 @@ module.exports = function (env) {
|
||||
path: path.resolve(__dirname, 'dist')
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".js", ".json", ".coffee"]
|
||||
extensions: [".js", ".ts", ".json", ".coffee"]
|
||||
},
|
||||
target: 'node',
|
||||
node: {
|
||||
@ -96,6 +96,14 @@ module.exports = function (env) {
|
||||
{
|
||||
test: /\.coffee$/,
|
||||
use: require.resolve('coffee-loader')
|
||||
},
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user