From 132f74bc7e73ba15a743f1f64bfafab8ade78230 Mon Sep 17 00:00:00 2001 From: David Worms Date: Wed, 3 Jan 2024 00:27:42 +0100 Subject: [PATCH] feat(core): custom coercion keyword --- packages/core/lib/actions/assert/schema.json | 3 +- .../lib/actions/execute/assert/schema.json | 6 +- packages/core/lib/actions/execute/index.js | 343 +++++++++++------- packages/core/lib/actions/execute/schema.json | 73 ++-- .../core/lib/actions/execute/wait/schema.json | 1 + .../core/lib/actions/fs/assert/schema.json | 15 +- .../core/lib/actions/fs/base/rmdir/index.js | 5 +- packages/core/lib/actions/fs/copy/schema.json | 6 +- packages/core/lib/actions/fs/glob/schema.json | 6 +- packages/core/lib/actions/fs/link/schema.json | 3 +- .../core/lib/actions/fs/mkdir/schema.json | 13 +- packages/core/lib/actions/fs/move/index.js | 53 +-- packages/core/lib/actions/fs/move/schema.json | 8 +- packages/core/lib/actions/fs/remove/index.js | 21 +- .../core/lib/actions/fs/remove/schema.json | 2 +- packages/core/lib/actions/fs/wait/index.js | 12 +- packages/core/lib/actions/fs/wait/schema.json | 6 +- .../core/lib/actions/ssh/open/schema.json | 3 +- packages/core/lib/actions/wait/index.js | 2 +- packages/core/lib/actions/wait/schema.json | 3 +- packages/core/lib/plugins/metadata/audit.js | 4 +- packages/core/lib/plugins/tools/schema.js | 8 +- .../plugins/tools/schema.keyword.cast_code.js | 2 + .../plugins/tools/schema.keyword.coercion.js | 75 ++++ packages/core/package.json | 2 +- .../core/test/actions/execute/assert.coffee | 2 + .../test/actions/execute/config.stdio.coffee | 15 +- packages/core/test/actions/fs/assert.coffee | 10 +- packages/core/test/actions/fs/copy.coffee | 8 +- packages/core/test/actions/fs/move.coffee | 8 + packages/core/test/actions/fs/wait.coffee | 7 + packages/core/test/actions/wait.coffee | 55 ++- .../core/test/plugins/execute.sudo.coffee | 12 +- .../test/plugins/tools/schema.coercion.coffee | 230 ++++++++++++ .../core/test/plugins/tools/schema.coffee | 2 +- .../core/test/plugins/tools/schema.ref.coffee | 6 +- packages/db/lib/database/schema.json | 1 + packages/docker/lib/compose/schema.json | 1 + packages/docker/lib/inspect/schema.json | 7 +- packages/docker/lib/run/schema.json | 15 + packages/docker/lib/tools/status/schema.json | 1 + packages/docker/lib/volume_create/schema.json | 2 + packages/file/lib/cache/schema.json | 2 + packages/file/lib/download/schema.json | 2 + packages/file/lib/index.js | 82 ++--- packages/file/lib/schema.json | 6 +- .../file/lib/types/locale_gen/schema.json | 1 + .../lib/types/ssh_authorized_keys/schema.json | 1 + packages/file/lib/utils/partial.js | 81 +---- packages/file/lib/yaml/schema.json | 1 + packages/ipa/lib/group/add_member/schema.json | 1 + packages/ipa/lib/user/find/schema.json | 10 + packages/ipa/lib/user/schema.json | 1 + packages/java/lib/keystore/remove/schema.json | 2 + packages/krb5/lib/ktutil/add/schema.json | 1 + packages/ldap/lib/acl/schema.json | 2 + packages/ldap/lib/add/schema.json | 1 + packages/ldap/lib/delete/schema.json | 1 + packages/ldap/lib/modify/schema.json | 3 + packages/ldap/lib/search/schema.json | 1 + packages/ldap/lib/user/index.js | 10 +- packages/ldap/lib/user/schema.json | 15 +- packages/network/lib/http/schema.json | 1 + packages/network/lib/http/wait/schema.json | 1 + packages/network/lib/tcp/wait/index.js | 46 ++- packages/network/lib/tcp/wait/schema.json | 54 +-- packages/network/test/http/index.coffee | 6 +- packages/network/test/tcp/wait.coffee | 6 +- packages/service/lib/install/schema.json | 2 + packages/service/lib/schema.json | 1 + packages/system/lib/cgroups/schema.json | 2 + packages/system/lib/info/disks/schema.json | 1 + packages/system/lib/mod/index.js | 11 +- packages/system/lib/mod/schema.json | 6 +- packages/system/lib/user/schema.json | 1 + packages/tools/lib/iptables/schema.json | 1 + packages/tools/lib/npm/schema.json | 1 + packages/tools/lib/npm/uninstall/schema.json | 1 + packages/tools/lib/npm/upgrade/schema.json | 1 + packages/tools/test/cron/add.coffee | 2 +- 80 files changed, 870 insertions(+), 555 deletions(-) create mode 100644 packages/core/lib/plugins/tools/schema.keyword.coercion.js create mode 100644 packages/core/test/plugins/tools/schema.coercion.coffee diff --git a/packages/core/lib/actions/assert/schema.json b/packages/core/lib/actions/assert/schema.json index 55e473d42..d6cb4b9f3 100644 --- a/packages/core/lib/actions/assert/schema.json +++ b/packages/core/lib/actions/assert/schema.json @@ -3,7 +3,8 @@ "type": "object", "properties": { "not": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": false, "description": "Negates the validation." }, diff --git a/packages/core/lib/actions/execute/assert/schema.json b/packages/core/lib/actions/execute/assert/schema.json index 321ab8d08..91fe889f5 100644 --- a/packages/core/lib/actions/execute/assert/schema.json +++ b/packages/core/lib/actions/execute/assert/schema.json @@ -3,7 +3,8 @@ "type": "object", "properties": { "code": { - "type": "array", + "type": ["array", "integer"], + "coercion": true, "items": { "type": "integer" }, @@ -12,7 +13,8 @@ "content": { "oneOf": [ { - "type": "string" + "type": ["string", "number"], + "coercion": true }, { "instanceof": "Buffer" diff --git a/packages/core/lib/actions/execute/index.js b/packages/core/lib/actions/execute/index.js index 30c7dd76a..a56278dd3 100644 --- a/packages/core/lib/actions/execute/index.js +++ b/packages/core/lib/actions/execute/index.js @@ -19,36 +19,31 @@ const errors = { // Action export default { - handler: async function({ + handler: async function ({ config, metadata, - tools: {find, log, path}, - ssh + tools: { find, log, path }, + ssh, }) { // Validate parameters config.mode ??= 0o500; - if (typeof config.command === 'function') { - config.command = (await this.call(config, config.command)); + if (typeof config.command === "function") { + config.command = await this.call(config, config.command); } if (config.bash === true) { - config.bash = 'bash'; + config.bash = "bash"; } if (config.arch_chroot === true) { - config.arch_chroot = 'arch-chroot'; + config.arch_chroot = "arch-chroot"; } if (config.command && config.trap) { config.command = `set -e\n${config.command}`; } config.command_original = `${config.command}`; - const dry = (await find(function({ - config: {dry} - }) { - return dry; - })); - if (['bash', 'arch_chroot'].filter(function(k) { - return config[k]; - }).length > 1) { - // TODO move next 2 lines this to schema or on_action ? + // TODO: implement a metadata.dry plugin + // const dry = await find(({ config: { dry } }) => dry); + if (config.bash && config.arch_chroot) { + // TODO: move next 2 lines this to schema or on_action ? throw Error("Incompatible properties: bash, arch_chroot"); } // Environment variables are merged with parent @@ -57,22 +52,15 @@ export default { const env_export = config.env_export != null ? config.env_export : !!ssh; let env_export_content = undefined; if (env_export && Object.keys(config.env).length) { - env_export_content = ( - (function() { - const results = []; - for (const k in config.env) { - const v = config.env[k]; - results.push(`export ${k}=${esa(v)}\n`); - } - return results; - })() - ).join('\n'); + env_export_content = Object.keys(config.env) + .map((k) => `export ${k}=${esa(config.env[k])}\n`) + .join("\n"); } // Guess current username - const current_username = utils.os.whoami({ssh}); + const current_username = utils.os.whoami({ ssh }); // Sudo if (config.sudo) { - if (current_username === 'root') { + if (current_username === "root") { config.sudo = false; } else { // Sudo commands are executed as a bash script unless arch_chroout is enabled @@ -83,13 +71,20 @@ export default { } // User substitution // Determines if writing is required and eventually convert uid to username - if (config.uid && current_username !== 'root' && !/\d/.test(`${config.uid}`)) { - const {stdout} = await this.execute({ - [`awk -v val=${config.uid} -F `]: " '$3==val{print $1}' /etc/passwd`" - }, function(err, {stdout}) {}); + if ( + config.uid && + current_username !== "root" && + !/\d/.test(`${config.uid}`) + ) { + const { stdout } = await this.execute( + { + [`awk -v val=${config.uid} -F `]: " '$3==val{print $1}' /etc/passwd`", + }, + function (err, { stdout }) {} + ); config.uid = stdout.trim(); if (!(config.bash || config.arch_chroot)) { - config.bash = 'bash'; + config.bash = "bash"; } } if (env_export && Object.keys(config.env).length) { @@ -98,28 +93,31 @@ export default { config.command = `source ${env_export_target}\n${config.command}`; log({ message: `Writing env export to ${JSON.stringify(env_export_target)}`, - level: 'INFO' + level: "INFO", }); await this.fs.base.writeFile({ $sudo: config.sudo, // Is it really necessary ? content: env_export_content, mode: config.mode, target: env_export_target, - uid: config.uid + uid: config.uid, }); } if (config.arch_chroot) { - // Note, with arch_chroot enabled, + // Note, with arch_chroot enabled, // arch_chroot_rootdir `/mnt` gave birth to // tmpdir `/mnt/tmpdir/nikita-random-path` // and target is inside it const command = config.command; - const target_in = path.join(config.arch_chroot_tmpdir, `execute-arch_chroot-${utils.string.hash(config.command)}`); + const target_in = path.join( + config.arch_chroot_tmpdir, + `execute-arch_chroot-${utils.string.hash(config.command)}` + ); const target = path.join(config.arch_chroot_rootdir, target_in); // target = "#{metadata.tmpdir}/#{utils.string.hash config.command}" if typeof config.target isnt 'string' log({ message: `Writing arch-chroot script to ${JSON.stringify(target)}`, - level: 'INFO' + level: "INFO", }); config.command = `${config.arch_chroot} ${config.arch_chroot_rootdir} bash ${target_in}`; if (config.sudo) { @@ -129,13 +127,16 @@ export default { $sudo: config.sudo, // Is it really necessary ? target: `${target}`, content: `${command}`, - mode: config.mode + mode: config.mode, }); - // Write script + // Write script } else if (config.bash) { const command = config.command; - const target = path.join(metadata.tmpdir, `execute-bash-${utils.string.hash(config.command)}`); - log('INFO', `Writing bash script to ${JSON.stringify(target)}`); + const target = path.join( + metadata.tmpdir, + `execute-bash-${utils.string.hash(config.command)}` + ); + log("INFO", `Writing bash script to ${JSON.stringify(target)}`); let cmd = `${config.bash} ${target}`; if (config.uid) { cmd = `su - ${config.uid} -c '${cmd}'`; @@ -162,18 +163,18 @@ export default { content: command, mode: config.mode, target: target, - uid: config.uid + uid: config.uid, }); } else if (config.sudo) { config.command = `sudo ${config.command}`; } // Execute - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { if (config.stdin_log) { log({ message: config.command_original, - type: 'stdin', - level: 'INFO' + type: "stdin", + level: "INFO", }); } const result = { @@ -181,14 +182,14 @@ export default { stdout: [], stderr: [], code: null, - command: config.command_original + command: config.command_original, }; if (config.dry) { return resolve(result); } const child = exec(config, { ssh: ssh, - env: config.env + env: config.env, }); if (config.stdin && child.stdin) { // Note, child[stdin|stdout|stderr] are undefined @@ -197,86 +198,108 @@ export default { } if (config.stdout && child.stdout) { child.stdout.pipe(config.stdout, { - end: false + end: false, }); } if (config.stderr && child.stderr) { child.stderr.pipe(config.stderr, { - end: false + end: false, }); } let stdout_stream_open = false; if (child.stdout && (config.stdout_return || config.stdout_log)) { - child.stdout.on('data', function(data) { + child.stdout.on("data", function (data) { if (config.stdout_log) { stdout_stream_open = true; } if (config.stdout_log) { log({ message: data, - type: 'stdout_stream' + type: "stdout_stream", }); } if (config.stdout_return) { - if (Array.isArray(result.stdout)) { // A string once `exit` is called + if (Array.isArray(result.stdout)) { + // A string once `exit` is called return result.stdout.push(data); } else { - return console.warn(['NIKITA_EXECUTE_STDOUT_INVALID:', 'stdout coming after child exit,', `got ${JSON.stringify(data.toString())},`, 'this is embarassing and we never found how to catch this bug,', 'we would really enjoy some help to replicate or fix this one.'].join(' ')); + return console.warn( + [ + "NIKITA_EXECUTE_STDOUT_INVALID:", + "stdout coming after child exit,", + `got ${JSON.stringify(data.toString())},`, + "this is embarassing and we never found how to catch this bug,", + "we would really enjoy some help to replicate or fix this one.", + ].join(" ") + ); } } }); } let stderr_stream_open = false; if (child.stderr && (config.stderr_return || config.stderr_log)) { - child.stderr.on('data', function(data) { + child.stderr.on("data", function (data) { if (config.stderr_log) { stderr_stream_open = true; } if (config.stderr_log) { log({ message: data, - type: 'stderr_stream' + type: "stderr_stream", }); } if (config.stderr_return) { - if (Array.isArray(result.stderr)) { // A string once `exit` is called + if (Array.isArray(result.stderr)) { + // A string once `exit` is called return result.stderr.push(data); } else { - return console.warn(['NIKITA_EXECUTE_STDERR_INVALID:', 'stderr coming after child exit,', `got ${JSON.stringify(data.toString())},`, 'this is embarassing and we never found how to catch this bug,', 'we would really enjoy some help to replicate or fix this one.'].join(' ')); + return console.warn( + [ + "NIKITA_EXECUTE_STDERR_INVALID:", + "stderr coming after child exit,", + `got ${JSON.stringify(data.toString())},`, + "this is embarassing and we never found how to catch this bug,", + "we would really enjoy some help to replicate or fix this one.", + ].join(" ") + ); } } }); } let exitCalled = false; - return child.on("exit", function(code) { + return child.on("exit", function (code) { if (exitCalled) return; exitCalled = true; - log('DEBUG', `Command exit with status: ${code}`); + log("DEBUG", `Command exit with status: ${code}`); result.code = code; // Give it some time because the "exit" event is sometimes called // before the "stdout" "data" event when running `npm test` - setImmediate(async function() { + setImmediate(async function () { if (stdout_stream_open && config.stdout_log) { log({ message: null, - type: 'stdout_stream' + type: "stdout_stream", }); } if (stderr_stream_open && config.stderr_log) { log({ message: null, - type: 'stderr_stream' + type: "stderr_stream", }); } - result.stdout = result.stdout.map(function(d) { - return d.toString(); - }).join(''); + result.stdout = result.stdout + .map(function (d) { + return d.toString(); + }) + .join(""); if (config.trim || config.stdout_trim) { result.stdout = result.stdout.trim(); } - result.stderr = result.stderr.map(function(d) { - return d.toString(); - }).join(''); + result.stderr = result.stderr + .map(function (d) { + return d.toString(); + }) + .join(""); if (config.trim || config.stderr_trim) { result.stderr = result.stderr.trim(); } @@ -285,16 +308,16 @@ export default { .format(result.stdout, config.format, result) .catch(reject); } - if (result.stdout && result.stdout !== '' && config.stdout_log) { + if (result.stdout && result.stdout !== "" && config.stdout_log) { log({ message: result.stdout, - type: 'stdout' + type: "stdout", }); } - if (result.stderr && result.stderr !== '' && config.stderr_log) { + if (result.stderr && result.stderr !== "" && config.stderr_log) { log({ message: result.stderr, - type: 'stderr' + type: "stderr", }); } if (child.stdout && config.stdout) { @@ -303,24 +326,51 @@ export default { if (child.stderr && config.stderr) { child.stderr.unpipe(config.stderr); } - if (config.code.true.indexOf(code) === -1 && config.code.false.indexOf(code) === -1) { + if ( + config.code.true.indexOf(code) === -1 && + config.code.false.indexOf(code) === -1 + ) { log({ - message: ['An unexpected exit code was encountered,', metadata.relax ? 'using relax mode,' : void 0, `command is ${JSON.stringify(utils.string.max(config.command_original, 50))},`, `got ${JSON.stringify(result.code)}`, `instead of ${JSON.stringify(config.code)}.`].filter(function(line) { - return !!line; - }).join(' '), - level: metadata.relax ? 'INFO' : 'ERROR' + message: [ + "An unexpected exit code was encountered,", + metadata.relax ? "using relax mode," : void 0, + `command is ${JSON.stringify( + utils.string.max(config.command_original, 50) + )},`, + `got ${JSON.stringify(result.code)}`, + `instead of ${JSON.stringify(config.code)}.`, + ] + .filter(function (line) { + return !!line; + }) + .join(" "), + level: metadata.relax ? "INFO" : "ERROR", }); - return reject(utils.error('NIKITA_EXECUTE_EXIT_CODE_INVALID', ['an unexpected exit code was encountered,', metadata.relax ? 'using relax mode,' : void 0, `command is ${JSON.stringify(utils.string.max(config.command_original, 50))},`, `got ${JSON.stringify(result.code)}`, `instead of ${JSON.stringify(config.code)}.`], { - ...result, - exit_code: code - })); + return reject( + utils.error( + "NIKITA_EXECUTE_EXIT_CODE_INVALID", + [ + "an unexpected exit code was encountered,", + metadata.relax ? "using relax mode," : void 0, + `command is ${JSON.stringify( + utils.string.max(config.command_original, 50) + )},`, + `got ${JSON.stringify(result.code)}`, + `instead of ${JSON.stringify(config.code)}.`, + ], + { + ...result, + exit_code: code, + } + ) + ); } if (config.code.false.indexOf(code) === -1) { result.$status = true; } else { log({ message: `Skip exit code \`${code}\``, - level: 'INFO' + level: "INFO", }); } return resolve(result); @@ -330,81 +380,100 @@ export default { }, hooks: { on_action: { - after: ['@nikitajs/core/plugins/execute', '@nikitajs/core/plugins/ssh', '@nikitajs/core/plugins/tools/path'], - before: ['@nikitajs/core/plugins/metadata/schema', '@nikitajs/core/plugins/metadata/tmpdir'], - handler: function({ - config, - metadata, - ssh, - tools: {find, path, walk} - }) { + after: [ + "@nikitajs/core/plugins/execute", + "@nikitajs/core/plugins/ssh", + "@nikitajs/core/plugins/tools/path", + ], + before: [ + "@nikitajs/core/plugins/metadata/schema", + "@nikitajs/core/plugins/metadata/tmpdir", + ], + handler: function ({ config, metadata, ssh, tools: { path } }) { if (config.env == null) { config.env = !ssh && !config.env ? process.env : {}; } - const env_export = config.env_export != null ? config.env_export : !!ssh; + const env_export = + config.env_export != null ? config.env_export : !!ssh; // Create the tmpdir if arch_chroot is activated if (config.arch_chroot && config.arch_chroot_rootdir) { - return metadata.tmpdir != null ? metadata.tmpdir : metadata.tmpdir = async function({os_tmpdir, tmpdir}) { - // Note, Arch mount `/tmp` with tmpfs in memory - // placing a file in the host fs will not expose it inside of chroot - config.arch_chroot_tmpdir = path.join('/opt', tmpdir); - tmpdir = path.join(config.arch_chroot_rootdir, config.arch_chroot_tmpdir); - const sudo = function(command) { - if (utils.os.whoami({ssh}) === 'root') { - return command; - } else { - return `sudo ${command}`; - } - }; - const command = ['set -e', sudo(`[ -w ${config.arch_chroot_rootdir} ] || exit 2;`), sudo(`mkdir -p ${tmpdir};`), sudo(`chmod 700 ${tmpdir};`)].join('\n'); - try { - await execPromise(ssh, command); - } catch (error) { - if (error.code === 2) { - throw errors.NIKITA_EXECUTE_ARCH_CHROOT_ROOTDIR_NOT_EXIST({ - err: error, - config: config - }); - } - throw error; - } - return { - target: tmpdir - }; - }; - } else if (config.sudo || config.bash || (env_export && Object.keys(config.env).length)) { - return metadata.tmpdir != null ? metadata.tmpdir : metadata.tmpdir = true; + return metadata.tmpdir != null + ? metadata.tmpdir + : (metadata.tmpdir = async function ({ os_tmpdir, tmpdir }) { + // Note, Arch mount `/tmp` with tmpfs in memory + // placing a file in the host fs will not expose it inside of chroot + config.arch_chroot_tmpdir = path.join("/opt", tmpdir); + tmpdir = path.join( + config.arch_chroot_rootdir, + config.arch_chroot_tmpdir + ); + const sudo = function (command) { + if (utils.os.whoami({ ssh }) === "root") { + return command; + } else { + return `sudo ${command}`; + } + }; + try { + await execPromise( + ssh, + [ + "set -e", + sudo(`[ -w ${config.arch_chroot_rootdir} ] || exit 2;`), + sudo(`mkdir -p ${tmpdir};`), + sudo(`chmod 700 ${tmpdir};`), + ].join("\n") + ); + } catch (error) { + if (error.code === 2) { + throw errors.NIKITA_EXECUTE_ARCH_CHROOT_ROOTDIR_NOT_EXIST({ + err: error, + config: config, + }); + } + throw error; + } + return { + target: tmpdir, + }; + }); + } else if ( + config.sudo || + config.bash || + (env_export && Object.keys(config.env).length) + ) { + return metadata.tmpdir != null + ? metadata.tmpdir + : (metadata.tmpdir = true); } - } + }, }, on_result: { - before: '@nikitajs/core/plugins/ssh', - handler: async function({ - action: {config, metadata, ssh} - }) { + before: "@nikitajs/core/plugins/ssh", + handler: async function ({ action: { config, metadata, ssh } }) { // Only arch chroot manage tmpdir, otherwise it is handled by the plugin if (!(config.arch_chroot && config.arch_chroot_rootdir)) { return; } // Disregard cleaning if tmpdir is a user defined function and if // the function failed to execute, see the on_action hook above. - if (typeof metadata.tmpdir === 'function') { + if (typeof metadata.tmpdir === "function") { return; } - const sudo = function(command) { - if (utils.os.whoami({ssh}) === 'root') { + const sudo = function (command) { + if (utils.os.whoami({ ssh }) === "root") { return command; } else { return `sudo ${command}`; } }; - const command = [sudo(`rm -rf ${metadata.tmpdir}`)].join('\n'); - return (await execPromise(ssh, command)); - } - } + const command = [sudo(`rm -rf ${metadata.tmpdir}`)].join("\n"); + return await execPromise(ssh, command); + }, + }, }, metadata: { - argument_to_config: 'command', - definitions: definitions - } + argument_to_config: "command", + definitions: definitions, + }, }; diff --git a/packages/core/lib/actions/execute/schema.json b/packages/core/lib/actions/execute/schema.json index 37956e4ad..2b45f1c36 100644 --- a/packages/core/lib/actions/execute/schema.json +++ b/packages/core/lib/actions/execute/schema.json @@ -3,39 +3,29 @@ "type": "object", "properties": { "arch_chroot": { - "type": [ - "boolean", - "string" - ], + "type": ["boolean", "string"], "description": "Run this command inside a root directory with the arc-chroot command or any provided string, require the \"arch_chroot_rootdir\" option if activated." }, "bash": { - "type": [ - "boolean", - "string" - ], + "type": ["boolean", "string"], "description": "Serialize the command into a file and execute it with bash." }, "code": { "cast_code": true, - "type": [ - "integer", - "string", - "array", - "object" - ], + "type": ["object", "array", "integer", "string"], + "coercion": true, "properties": { "true": { - "type": "array", + "type": ["array", "integer"], + "coercion": true, "items": { "type": "integer" }, - "default": [ - 0 - ] + "default": [0] }, "false": { - "type": "array", + "type": ["array", "integer"], + "coercion": true, "items": { "type": "integer" }, @@ -61,12 +51,14 @@ "description": "Current working directory from where to execute the command." }, "dirty": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": false, "description": "Leave temporary files on the filesystem." }, "dry": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "description": "Run the action without executing any real command." }, "env": { @@ -79,7 +71,8 @@ } }, "env_export": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "description": "Write a temporary file which exports the the environment variables defined in the `env` property. The value is always `true` when environment variables must be used with SSH." }, "format": { @@ -103,7 +96,8 @@ "description": "Unix group id." }, "stdio": { - "type": "array", + "type": ["array", "integer", "string"], + "coercion": true, "items": { "$ref": "#/definitions/stdio" }, @@ -114,7 +108,8 @@ "description": "Readable EventEmitter in which the standard input is piped from." }, "stdin_log": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": true, "description": "Log the executed command of type stdin, default is `true`." }, @@ -123,17 +118,20 @@ "description": "Writable EventEmitter in which the standard output of executed commands will be piped." }, "stdout_return": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": true, "description": "Return the stderr content in the output, default is `true`. It is preferable to set this property to `false` and to use the `stdout` property when expecting a large stdout output." }, "stdout_log": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": true, "description": "Pass stdout output to the logs of type \"stdout_stream\", default is `true`." }, "stdout_trim": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": false, "description": "Trim the stdout output." }, @@ -142,22 +140,26 @@ "description": "Writable EventEmitter in which the standard error output of executed command will be piped." }, "stderr_return": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": true, "description": "Return the stderr content in the output, default is `true`. It is preferable to set this property to `false` and to use the `stderr` property when expecting a large stderr output." }, "stderr_log": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": true, "description": "Pass stdout output to the logs of type \"stdout_stream\", default is `true`." }, "stderr_trim": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": false, "description": "Trim the stderr output." }, "sudo": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "description": "Run a command as sudo, desactivated if user is \"root\"." }, "target": { @@ -165,17 +167,20 @@ "description": "Temporary path storing the script, only apply with the `bash` and `arch_chroot` properties, always disposed once executed. Unless provided, the default location is `{metadata.tmpdir}/{string.hash config.command}`. See the `tmpdir` plugin for additionnal information." }, "trap": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": false, "description": "Exit immediately if a commands inside a script exits with a non-zero exit status, add the `set -e` option to your script." }, "trim": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": false, "description": "Trim both the stdout and stderr outputs." }, "uid": { - "type": "integer", + "type": ["integer", "string"], + "coercion": true, "description": "Unix user id." } }, diff --git a/packages/core/lib/actions/execute/wait/schema.json b/packages/core/lib/actions/execute/wait/schema.json index 80c409433..d537f33ad 100644 --- a/packages/core/lib/actions/execute/wait/schema.json +++ b/packages/core/lib/actions/execute/wait/schema.json @@ -11,6 +11,7 @@ }, "command": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/core/lib/actions/fs/assert/schema.json b/packages/core/lib/actions/fs/assert/schema.json index 09ea1bde9..bfe1f01be 100644 --- a/packages/core/lib/actions/fs/assert/schema.json +++ b/packages/core/lib/actions/fs/assert/schema.json @@ -23,6 +23,7 @@ }, "filetype": { "type": "array", + "coercion": true, "default": [], "items": { "type": [ @@ -34,6 +35,7 @@ }, "filter": { "type": "array", + "coercion": true, "default": [], "items": { "instanceof": "RegExp" @@ -41,10 +43,8 @@ "description": "Text to filter in actual content before matching." }, "gid": { - "type": [ - "integer", - "string" - ], + "type": ["integer", "string"], + "coercion": true, "description": "Group ID to assert." }, "md5": { @@ -53,6 +53,7 @@ }, "mode": { "type": "array", + "coercion": true, "items": { "$ref": "module://@nikitajs/core/actions/fs/base/chmod#/definitions/config/properties/mode" }, @@ -79,10 +80,8 @@ "description": "Trim the actual and expected content before matching." }, "uid": { - "type": [ - "integer", - "string" - ], + "type": ["integer", "string"], + "coercion": true, "description": "User ID to assert." } }, diff --git a/packages/core/lib/actions/fs/base/rmdir/index.js b/packages/core/lib/actions/fs/base/rmdir/index.js index aac58d91f..7e76bbb7a 100644 --- a/packages/core/lib/actions/fs/base/rmdir/index.js +++ b/packages/core/lib/actions/fs/base/rmdir/index.js @@ -24,10 +24,7 @@ export default { await this.execute({ command: [`[ ! -d ${esa(config.target)} ] && exit 2`, !config.recursive ? `rmdir ${esa(config.target)}` : `rm -R ${esa(config.target)}`].join('\n') }); - log({ - message: "Directory successfully removed", - level: 'INFO' - }); + log("INFO", "Directory successfully removed"); } catch (error) { if (error.exit_code === 2) { error = errors.NIKITA_FS_RMDIR_TARGET_ENOENT({ diff --git a/packages/core/lib/actions/fs/copy/schema.json b/packages/core/lib/actions/fs/copy/schema.json index 7d88d39f9..1bd4b8cde 100644 --- a/packages/core/lib/actions/fs/copy/schema.json +++ b/packages/core/lib/actions/fs/copy/schema.json @@ -11,7 +11,8 @@ "parent": { "oneOf": [ { - "type": "boolean" + "type": ["boolean", "number", "string"], + "coercion": true }, { "type": "object", @@ -31,7 +32,8 @@ "description": "Create parent directory with provided attributes if an object or\ndefault system config if \"true\", supported attributes include 'mode',\n'uid', 'gid', 'size', 'atime', and 'mtime'." }, "preserve": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": false, "description": "Preserve file ownerships and permissions." }, diff --git a/packages/core/lib/actions/fs/glob/schema.json b/packages/core/lib/actions/fs/glob/schema.json index 0c0dcca31..0ec037601 100644 --- a/packages/core/lib/actions/fs/glob/schema.json +++ b/packages/core/lib/actions/fs/glob/schema.json @@ -3,7 +3,8 @@ "type": "object", "properties": { "dot": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "description": "Minimatch option to handle files starting with a \".\"." }, "target": { @@ -11,7 +12,8 @@ "description": "Globbing expression of the directory tree to match." }, "trailing": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "default": false, "description": "Leave a slash at the end of directories." }, diff --git a/packages/core/lib/actions/fs/link/schema.json b/packages/core/lib/actions/fs/link/schema.json index 853f2f9e1..1b7cc023e 100644 --- a/packages/core/lib/actions/fs/link/schema.json +++ b/packages/core/lib/actions/fs/link/schema.json @@ -11,7 +11,8 @@ "description": "Symbolic link to be created." }, "exec": { - "type": "boolean", + "type": ["boolean", "number", "string"], + "coercion": true, "description": "Create an executable file with an `exec` command." }, "mode": { diff --git a/packages/core/lib/actions/fs/mkdir/schema.json b/packages/core/lib/actions/fs/mkdir/schema.json index 9877af69b..0be9d2942 100644 --- a/packages/core/lib/actions/fs/mkdir/schema.json +++ b/packages/core/lib/actions/fs/mkdir/schema.json @@ -3,10 +3,7 @@ "type": "object", "properties": { "cwd": { - "type": [ - "boolean", - "string" - ], + "type": ["boolean", "string"], "description": "Current working directory for relative paths. A boolean value only\napply without an SSH connection and default to `process.cwd()`." }, "exclude": { @@ -14,9 +11,8 @@ "description": "Exclude directories matching a regular expression. For example, the\nexpression `/${/` on './var/cache/${user}' exclude the directories\ncontaining a variables and only apply to `./var/cache/`." }, "force": { - "type": [ - "boolean" - ], + "type": ["boolean", "number", "string"], + "coercion": true, "description": "Overwrite permissions on the target directory. By default,\npermissions on only set on directory creation. It does not impact\nthe parent directory permissions." }, "gid": { @@ -28,7 +24,8 @@ "parent": { "oneOf": [ { - "type": "boolean" + "type": ["boolean", "number", "string"], + "coercion": true }, { "type": "object", diff --git a/packages/core/lib/actions/fs/move/index.js b/packages/core/lib/actions/fs/move/index.js index cb66d69d7..b865eb608 100644 --- a/packages/core/lib/actions/fs/move/index.js +++ b/packages/core/lib/actions/fs/move/index.js @@ -1,15 +1,12 @@ // Dependencies import definitions from "./schema.json" assert { type: "json" }; -// Exports +// Action export default { handler: async function ({ config, tools: { log, path } }) { const { exists } = await this.fs.base.exists(config.target); if (!exists) { - log({ - message: `Rename ${config.source} to ${config.target}`, - level: "WARN", - }); + log("WARN", `Rename ${config.source} to ${config.target}`); await this.fs.base.rename({ source: config.source, target: config.target, @@ -17,17 +14,11 @@ export default { return true; } if (config.force) { - log({ - message: `Remove ${config.target}`, - level: "WARN", - }); + log("WARN", `Remove ${config.target}`); await this.fs.remove({ target: config.target, }); - log({ - message: `Rename ${config.source} to ${config.target}`, - level: "WARN", - }); + log("WARN", `Rename ${config.source} to ${config.target}`); await this.fs.base.rename({ source: config.source, target: config.target, @@ -35,55 +26,33 @@ export default { return true; } if (!config.target_md5) { - log({ - message: "Get target md5", - level: "DEBUG", - }); + log("DEBUG", "Get target md5"); const { hash } = await this.fs.hash(config.target); - log({ - message: 'Destination md5 is "hash"', - level: "INFO", - }); + log("INFO", 'Destination md5 is "hash"'); config.target_md5 = hash; } if (!config.source_md5) { - log({ - message: "Get source md5", - level: "DEBUG", - }); + log("DEBUG", "Get source md5"); const { hash } = await this.fs.hash(config.source); - log({ - message: 'Source md5 is "hash"', - level: "INFO", - }); + log("INFO", 'Source md5 is "hash"'); config.source_md5 = hash; } if (config.source_md5 === config.target_md5) { - log({ - message: `Remove ${config.source}`, - level: "WARN", - }); + log("WARN", `Remove ${config.source}`); await this.fs.remove({ target: config.source, }); return false; } - log({ - message: `Remove ${config.target}`, - level: "WARN", - }); + log("WARN", `Remove ${config.target}`); await this.fs.remove({ target: config.target, }); - log({ - message: `Rename ${config.source} to ${config.target}`, - level: "WARN", - }); + log("WARN", `Rename ${config.source} to ${config.target}`); await this.fs.base.rename({ source: config.source, target: config.target, }); - return {}; }, metadata: { definitions: definitions, diff --git a/packages/core/lib/actions/fs/move/schema.json b/packages/core/lib/actions/fs/move/schema.json index cf2a9b373..d35206ad4 100644 --- a/packages/core/lib/actions/fs/move/schema.json +++ b/packages/core/lib/actions/fs/move/schema.json @@ -3,7 +3,8 @@ "type": "object", "properties": { "force": { - "type": "boolean", + "type": ["boolean", "integer", "string"], + "coercion": true, "default": false, "description": "Force the replacement of the file without checksum verification, speed\\nup the action and disable the `moved` indicator in the callback." }, @@ -24,9 +25,6 @@ "description": "Destination md5 checkum if known, otherwise computed if target exists." } }, - "required": [ - "source", - "target" - ] + "required": ["source", "target"] } } diff --git a/packages/core/lib/actions/fs/remove/index.js b/packages/core/lib/actions/fs/remove/index.js index f2ea50e6e..4d18cf467 100644 --- a/packages/core/lib/actions/fs/remove/index.js +++ b/packages/core/lib/actions/fs/remove/index.js @@ -1,32 +1,25 @@ // Dependencies import utils from '@nikitajs/core/utils'; import definitions from "./schema.json" assert { type: "json" }; +const esa = utils.string.escapeshellarg; -// Exports +// Action export default { handler: async function ({ config, tools: { log } }) { - // Start real work const { files } = await this.fs.glob(config.target); for (const file of files) { - log({ - message: `Removing file ${file}`, - level: "INFO", - }); + log("INFO", `Removing file ${esa(file)}.`); try { const { status } = await this.execute({ command: [ "rm", "-d", // Attempt to remove directories as well as other types of files. - config.recursive ? "-r" : void 0, - file, - // "rm -rf '#{file}'" - ].join(" "), + config.recursive && "-r", + esa(file), + ].filter(Boolean).join(" "), }); if (status) { - log({ - message: `File ${file} removed`, - level: "WARN", - }); + log("WARN", `File ${esa(file)} removed.`); } } catch (error) { if (utils.string.lines(error.stderr.trim()).length === 1) { diff --git a/packages/core/lib/actions/fs/remove/schema.json b/packages/core/lib/actions/fs/remove/schema.json index 0a776b20a..583a7e242 100644 --- a/packages/core/lib/actions/fs/remove/schema.json +++ b/packages/core/lib/actions/fs/remove/schema.json @@ -7,7 +7,7 @@ "description": "Attempt to remove the file hierarchy rooted in the directory.\\nAttempting to remove a non-empty directory without the `recursive`\\nconfig will throw an Error." }, "target": { - "type": "string", + "$ref": "module://@nikitajs/core/actions/fs/glob#/definitions/config/properties/target", "description": "File, directory or glob (pattern matching based on wildcard\\ncharacters)." } }, diff --git a/packages/core/lib/actions/fs/wait/index.js b/packages/core/lib/actions/fs/wait/index.js index 515e1e881..4d04212e6 100644 --- a/packages/core/lib/actions/fs/wait/index.js +++ b/packages/core/lib/actions/fs/wait/index.js @@ -1,7 +1,7 @@ // Dependencies import definitions from "./schema.json" assert { type: "json" }; -// Exports +// Action export default { handler: async function ({ config, tools: { log } }) { let status = false; @@ -27,16 +27,6 @@ export default { } return status; }, - hooks: { - on_action: { - after: "@nikitajs/core/plugins/metadata/argument_to_config", - handler: function ({ config }) { - if (typeof config.target === "string") { - return (config.target = [config.target]); - } - }, - }, - }, metadata: { argument_to_config: "target", definitions: definitions, diff --git a/packages/core/lib/actions/fs/wait/schema.json b/packages/core/lib/actions/fs/wait/schema.json index 61c34c5dd..9363f6944 100644 --- a/packages/core/lib/actions/fs/wait/schema.json +++ b/packages/core/lib/actions/fs/wait/schema.json @@ -4,15 +4,17 @@ "properties": { "target": { "type": "array", + "coercion": true, "items": { "type": "string" }, "description": "Paths to the files and directories." }, "interval": { - "type": "integer", + "type": ["integer", "string"], + "coercion": true, "default": 2000, - "description": "Time interval between which we should wait before re-executing the\ncheck, default to 2s." + "description": "Time interval between which we should wait before re-executing the check, default to 2s." } }, "required": [ diff --git a/packages/core/lib/actions/ssh/open/schema.json b/packages/core/lib/actions/ssh/open/schema.json index f82032022..9e3dda101 100644 --- a/packages/core/lib/actions/ssh/open/schema.json +++ b/packages/core/lib/actions/ssh/open/schema.json @@ -24,7 +24,8 @@ "description": "Password of the user used to authenticate and create the SSH\nconnection." }, "port": { - "type": "integer", + "type": ["integer", "string"], + "coercion": true, "default": 22, "description": "Port of the remote server." }, diff --git a/packages/core/lib/actions/wait/index.js b/packages/core/lib/actions/wait/index.js index d8510a9d5..673b6fcc1 100644 --- a/packages/core/lib/actions/wait/index.js +++ b/packages/core/lib/actions/wait/index.js @@ -42,7 +42,7 @@ export default { $status: attempts > 1, }; } catch (err) { - log("WARN" `Attempt #${attempts} failed with message: ${err.message}`); + log("WARN", `Attempt #${attempts} failed with message: ${err.message}`); await wait(config.interval); } } diff --git a/packages/core/lib/actions/wait/schema.json b/packages/core/lib/actions/wait/schema.json index ada335b35..73161c081 100644 --- a/packages/core/lib/actions/wait/schema.json +++ b/packages/core/lib/actions/wait/schema.json @@ -4,7 +4,8 @@ "oneOf": [{ "properties": { "time": { - "type": "integer", + "type": ["integer", "string"], + "coercion": true, "description": "Time in millisecond to wait for." } }, diff --git a/packages/core/lib/plugins/metadata/audit.js b/packages/core/lib/plugins/metadata/audit.js index 1cbdaeafa..b9ee88f24 100644 --- a/packages/core/lib/plugins/metadata/audit.js +++ b/packages/core/lib/plugins/metadata/audit.js @@ -155,7 +155,7 @@ export default { ws: ws, listeners: { action: function ({ action, error }) { - const message = action.metadata.namespace?.join(".") || action.module; + const message = action.metadata.namespace?.join(".") || action.metadata.module || "nikita"; const color = error ? audit.colors.error : audit.colors.info; action.parent.state.audit.index++; print( @@ -241,7 +241,7 @@ export default { { color: error ? audit.colors.error : audit.colors.info, prefix: "ACTION", - message: action.metadata.namespace?.join(".") || action.module || "nikita", + message: action.metadata.namespace?.join(".") || action.metadata.module || "nikita", index: action.metadata.index, position: [], side: string.print_time( diff --git a/packages/core/lib/plugins/tools/schema.js b/packages/core/lib/plugins/tools/schema.js index 0b6a614c0..c9817c5d1 100644 --- a/packages/core/lib/plugins/tools/schema.js +++ b/packages/core/lib/plugins/tools/schema.js @@ -12,8 +12,9 @@ import ajv_keywords from "ajv-keywords"; import ajv_formats from "ajv-formats"; import utils from "@nikitajs/core/utils"; import instanceofDef from "ajv-keywords/dist/definitions/instanceof.js"; -import cast_code from '@nikitajs/core/plugins/tools/schema.keyword.cast_code' -import filemode from '@nikitajs/core/plugins/tools/schema.keyword.filemode' +import cast_code from '@nikitajs/core/plugins/tools/schema.keyword.cast_code'; +import coercion from '@nikitajs/core/plugins/tools/schema.keyword.coercion'; +import filemode from '@nikitajs/core/plugins/tools/schema.keyword.filemode'; instanceofDef.CONSTRUCTORS["Error"] = Error; instanceofDef.CONSTRUCTORS["stream.Writable"] = stream.Writable; @@ -54,7 +55,7 @@ export default { allowUnionTypes: true, // eg type: ['boolean', 'integer'] strict: true, strictRequired: false, // see https://github.com/ajv-validator/ajv/issues/1571 - coerceTypes: 'array', + // coerceTypes: 'array', loadSchema: (uri) => new Promise(async function (accept, reject) { let pathname, protocol; @@ -112,6 +113,7 @@ export default { ajv_formats(ajv); // Note, this is currently tested in action.execute.config.code ajv.addKeyword(cast_code); + ajv.addKeyword(coercion); ajv.addKeyword(filemode); action.tools.schema = { ajv: ajv, diff --git a/packages/core/lib/plugins/tools/schema.keyword.cast_code.js b/packages/core/lib/plugins/tools/schema.keyword.cast_code.js index 5cf607c9b..2dd77f3a5 100644 --- a/packages/core/lib/plugins/tools/schema.keyword.cast_code.js +++ b/packages/core/lib/plugins/tools/schema.keyword.cast_code.js @@ -1,3 +1,5 @@ +import utils from "@nikitajs/core/utils"; + export default { keyword: "cast_code", type: ["integer", "string", "array", "object"], diff --git a/packages/core/lib/plugins/tools/schema.keyword.coercion.js b/packages/core/lib/plugins/tools/schema.keyword.coercion.js new file mode 100644 index 000000000..5c7e4d6de --- /dev/null +++ b/packages/core/lib/plugins/tools/schema.keyword.coercion.js @@ -0,0 +1,75 @@ + +import codegen from 'ajv/dist/compile/codegen/index.js'; +import errors from 'ajv/dist/compile/errors.js'; + +export default { + keyword: "coercion", + modifying: true, + code: (cxt) => { + const assignParentData = function ( + { gen, parentData, parentDataProperty }, + expr + ) { + gen.if(codegen._`${parentData} !== undefined`, () => + gen.assign(codegen._`${parentData}[${parentDataProperty}]`, expr) + ); + }; + const { data, gen, parentSchema, it } = cxt; + const coerced = gen.let("coerced", codegen._`undefined`); + const types = Array.isArray(parentSchema.type) + ? parentSchema.type + : [parentSchema.type]; + switch (types[0]) { + case "array": + gen.if(codegen._`!Array.isArray(${data})`, () => { + gen.assign(coerced, codegen._`[${data}]`); + }); + break; + case "boolean": + gen.if( + codegen._`typeof ${data} === "string" || typeof ${data} === "number"`, + () => { + gen.assign(coerced, codegen._`${data} != ""`); + } + ); + break; + case "number": + case "integer": + gen.if(codegen._`typeof ${data} === "string"`, () => { + gen.if( + codegen._`isNaN(${data})`, + () => { + errors.reportError(cxt, { + message: `fail to convert string to ${types[0]}`, + params: ({ data }) => codegen._`{value: ${data}}`, + }); + }, + () => { + gen.assign(coerced, codegen._`+${data}`); + } + ); + }); + break; + case "string": + // Not, boolean coercion should not be enabled by default but with + // `{..., coercion: ["boolean_to_string"] }` + // Or: + // `{..., coercion: {boolean_to_string: true} }` + gen.if(codegen._`typeof ${data} === "boolean"`, () => { + gen.assign(coerced, codegen._`${data} ? "1" : ""`); + }); + gen.if(codegen._`typeof ${data} === "number"`, () => { + gen.assign(coerced, codegen._`"" +${data}`); + }); + break; + } + gen.if(codegen._`${coerced} !== undefined`, () => { + gen.assign(data, coerced); + assignParentData(it, coerced); + }); + }, + metaSchema: { + type: "boolean", + enum: [true], + }, +}; diff --git a/packages/core/package.json b/packages/core/package.json index d8567fbbe..923245ed7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -119,7 +119,7 @@ "should" ], "throw-deprecation": true, - "timeout": 10000 + "timeout": 40000 }, "repository": { "type": "git", diff --git a/packages/core/test/actions/execute/assert.coffee b/packages/core/test/actions/execute/assert.coffee index ad635be87..a66ddc7b3 100644 --- a/packages/core/test/actions/execute/assert.coffee +++ b/packages/core/test/actions/execute/assert.coffee @@ -12,9 +12,11 @@ describe 'actions.execute.assert', -> it 'coercion', -> nikita.execute.assert command: 'exit 1' + content: 42 code: 1 ({config}) -> config.code.should.eql [1] + config.content.should.eql "42" describe 'exit code', -> diff --git a/packages/core/test/actions/execute/config.stdio.coffee b/packages/core/test/actions/execute/config.stdio.coffee index 1dbfc2e8e..257cf6b6c 100644 --- a/packages/core/test/actions/execute/config.stdio.coffee +++ b/packages/core/test/actions/execute/config.stdio.coffee @@ -13,21 +13,24 @@ describe 'actions.execute.config.stdio', -> stdio: 1234 .should.be.rejectedWith code: 'NIKITA_SCHEMA_VALIDATION_CONFIG' - + it 'valid string', -> nikita.execute command: 'abc' stdio: 'overlapped' - , (->) - + , ({config})-> + config.stdio.should.eql [ 'overlapped' ] + it 'valid integer', -> nikita.execute command: 'abc' stdio: 1 - , (->) - + , ({config})-> + config.stdio.should.eql [ 1 ] + it 'valid array', -> nikita.execute command: 'abc' stdio: ['overlapped', 'overlapped'] - , (->) + , ({config})-> + config.stdio.should.eql ['overlapped', 'overlapped'] diff --git a/packages/core/test/actions/fs/assert.coffee b/packages/core/test/actions/fs/assert.coffee index 0d361ccf2..bb9168c01 100644 --- a/packages/core/test/actions/fs/assert.coffee +++ b/packages/core/test/actions/fs/assert.coffee @@ -12,10 +12,18 @@ describe 'actions.fs.assert', -> it 'coersion', -> nikita.fs.assert - mode: '744', + filetype: 'file' + filter: /filter / + gid: '1000' + mode: '744' + uid: '1000' target: '/tmp/fake' , ({config}) -> + config.filetype.should.eql [ 'file' ] + config.filter.should.eql [ /filter / ] + config.gid.should.eql 1000 config.mode.should.eql [ 0o0744 ] + config.uid.should.eql 1000 describe 'exists', -> diff --git a/packages/core/test/actions/fs/copy.coffee b/packages/core/test/actions/fs/copy.coffee index 1064944af..5929c0cf4 100644 --- a/packages/core/test/actions/fs/copy.coffee +++ b/packages/core/test/actions/fs/copy.coffee @@ -237,7 +237,7 @@ describe 'actions.fs.copy', -> describe 'directory', -> - they 'should copy without slash at the end', ({ssh}) -> + they 'copy without slash at the end', ({ssh}) -> nikita $ssh: ssh $tmpdir: true @@ -262,7 +262,7 @@ describe 'actions.fs.copy', -> await @fs.assert "#{tmpdir}/target_2/source/a_dir/a_file" await @fs.assert "#{tmpdir}/target_2/source/a_file" - they 'should copy the files when dir end with slash', ({ssh}) -> + they 'copy the files when dir end with slash', ({ssh}) -> nikita $ssh: ssh $tmpdir: true @@ -287,7 +287,7 @@ describe 'actions.fs.copy', -> await @fs.assert "#{tmpdir}/target_2/a_dir/a_file" await @fs.assert "#{tmpdir}/target_2/a_file" - they 'should copy hidden files', ({ssh}) -> + they 'copy hidden files', ({ssh}) -> nikita $ssh: ssh $tmpdir: true @@ -375,7 +375,7 @@ describe 'actions.fs.copy', -> target: "#{tmpdir}/a_target/a_dir/a_file" mode: 0o0655 - they.skip 'should copy with globing and hidden files', ({ssh}) -> + they.skip 'copy with globing and hidden files', ({ssh}) -> # Todo: not yet implemented nikita $ssh: ssh diff --git a/packages/core/test/actions/fs/move.coffee b/packages/core/test/actions/fs/move.coffee index b727739d7..dcdd4d791 100644 --- a/packages/core/test/actions/fs/move.coffee +++ b/packages/core/test/actions/fs/move.coffee @@ -7,6 +7,14 @@ they = mochaThey(test.config) describe 'actions.fs.move', -> return unless test.tags.posix + it 'coercion', -> + nikita.fs.move + force: 1 + source: "fake_source" + target: "fake_target" + , ({config}) -> config.force.should.be.true() + + they 'error missing target', ({ssh}) -> nikita $ssh: ssh diff --git a/packages/core/test/actions/fs/wait.coffee b/packages/core/test/actions/fs/wait.coffee index 150a1d2f1..142883331 100644 --- a/packages/core/test/actions/fs/wait.coffee +++ b/packages/core/test/actions/fs/wait.coffee @@ -13,6 +13,13 @@ describe 'actions.fs.wait', -> nikita.fs.wait '/path/to/file', ({config}) -> config.target.should.eql ['/path/to/file'] + it 'coercion', -> + nikita.fs.wait + interval: '1000' + target: 'fake' + , ({config}) -> + config.interval.should.eql 1000 + describe 'usage', -> they 'status false if already exists', ({ssh}) -> diff --git a/packages/core/test/actions/wait.coffee b/packages/core/test/actions/wait.coffee index fb53fb882..974d378eb 100644 --- a/packages/core/test/actions/wait.coffee +++ b/packages/core/test/actions/wait.coffee @@ -7,38 +7,37 @@ they = mochaThey(test.config) describe 'actions.wait', -> return unless test.tags.api - describe 'time', -> + describe 'schema', -> - describe 'schema', -> + it 'argument integer is interpreted as time', -> + await nikita.wait 10 - it 'argument is integer', -> - await nikita - .wait 10 + it 'coercion', -> + await nikita + .wait '10', $handler: ({config}) => config.time.should.eql 10 + await nikita + .wait time: '10', $handler: ({config}) => config.time.should.eql 10 - it 'arguments contains a function', -> - # In such case, the function is treated as an child action and it receives - # the configuration from its parent. - await nikita - .wait {a: 1}, ({config}) => config.a.should.eql 1 + it 'arguments contains a function', -> + # In such case, the function is treated as an child action and it receives + # the configuration from its parent. + await nikita + .wait {a: 1}, ({config}) => config.a.should.eql 1 - it 'time string is converted to integer', -> - # In such case, the function is treated as an child action and it receives - # the configuration from its parent. - await nikita - .wait time: '10', $handler: ({config}) => console.log ':final:config:', config + they 'validate time argument', ({ssh}) -> + before = Date.now() + nikita + $ssh: ssh + .wait + time: 'an': 'object' + .should.be.rejectedWith [ + 'NIKITA_SCHEMA_VALIDATION_CONFIG: multiple errors were found in the configuration of action `wait`:' + '#/definitions/config/oneOf config must match exactly one schema in oneOf, passingSchemas is null;' + '#/definitions/config/oneOf/0/properties/time/type config/time must be integer,string, type is ["integer","string"];' + '#/definitions/config/oneOf/1/additionalProperties config must NOT have additional properties, additionalProperty is "time".' + ].join ' ' - they 'validate argument', ({ssh}) -> - before = Date.now() - nikita - $ssh: ssh - .wait - time: 'an': 'object' - .should.be.rejectedWith [ - 'NIKITA_SCHEMA_VALIDATION_CONFIG: multiple errors were found in the configuration of action `wait`:' - '#/definitions/config/oneOf config must match exactly one schema in oneOf, passingSchemas is null;' - '#/definitions/config/oneOf/0/properties/time/type config/time must be integer, type is "integer";' - '#/definitions/config/oneOf/1/additionalProperties config must NOT have additional properties, additionalProperty is "time".' - ].join ' ' + describe 'time', -> they 'as main argument integer', ({ssh}) -> nikita @@ -47,7 +46,7 @@ describe 'actions.wait', -> {before} = await @call -> before: Date.now() await @wait 200 - await @wait '200' + await @wait 200 await @wait 0 interval = Date.now() - before (interval >= 400 and interval < 600).should.be.true() diff --git a/packages/core/test/plugins/execute.sudo.coffee b/packages/core/test/plugins/execute.sudo.coffee index 069b96507..767497f71 100644 --- a/packages/core/test/plugins/execute.sudo.coffee +++ b/packages/core/test/plugins/execute.sudo.coffee @@ -30,7 +30,7 @@ describe 'plugins.execute.sudo', -> $sudo: true # Note, we are testing EACCESS error because it is impossible to do it # without sudo inside fs.readFile - @fs.base.readFile + await @fs.base.readFile target: "#{tmpdir}/a_file" encoding: 'ascii' .should.be.rejectedWith @@ -70,7 +70,7 @@ describe 'plugins.execute.sudo', -> they 'writeFile', ({ssh}) -> nikita $ssh: ssh - $tmpdir: '/tmp/nikita' + $tmpdir: true , ({metadata: {tmpdir}})-> await @fs.base.mkdir target: "#{tmpdir}/a_dir" @@ -79,19 +79,19 @@ describe 'plugins.execute.sudo', -> uid: 0 gid: 0 $sudo: true - @fs.base.writeFile + await @fs.base.writeFile target: "#{tmpdir}/a_dir/a_file" content: 'some content' $sudo: true - @fs.base.readFile + await @fs.base.readFile target: "#{tmpdir}/a_dir/a_file" encoding: 'ascii' $sudo: true .should.resolvedWith data: 'some content' - @fs.base.unlink + await @fs.base.unlink target: "#{tmpdir}/a_dir/a_file" $sudo: true - @fs.base.rmdir + await @fs.base.rmdir target: "#{tmpdir}/a_dir" $sudo: true diff --git a/packages/core/test/plugins/tools/schema.coercion.coffee b/packages/core/test/plugins/tools/schema.coercion.coffee new file mode 100644 index 000000000..33012f832 --- /dev/null +++ b/packages/core/test/plugins/tools/schema.coercion.coffee @@ -0,0 +1,230 @@ + +import nikita from '@nikitajs/core' +import test from '../../test.coffee' + +describe 'plugins.tools.schema.coercion', -> + return unless test.tags.api + + describe 'integer', -> + + it 'from string', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_string': + type: ['number', 'string'] + coercion: true + from_string: '123' + , ({config}) -> + config.from_string.should.eql 123 + + describe 'number', -> + + it 'from string', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_string': + type: ['number', 'string'] + coercion: true + from_string: '1.23' + , ({config}) -> + config.from_string.should.eql 1.23 + + it 'from string (invalid)', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_string': + type: ['number', 'string'] + coercion: true + from_string: 'abc' + .should.be.rejectedWith [ + 'NIKITA_SCHEMA_VALIDATION_CONFIG:' + 'one error was found in the configuration of root action:' + '#/definitions/config/properties/from_string/coercion config/from_string' + 'fail to convert string to number,' + 'value is "abc".' + ].join(' ') + + it 'dont conflict with object', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_object': + type: ['number', 'object'] + coercion: true + from_object: {key: 'value'} + , ({config}) -> + config.from_object.should.eql {key: 'value'} + + describe 'string', -> + + it 'from boolean', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_boolean_false': + type: ['string', 'boolean'] + coercion: true + 'from_boolean_true': + type: ['string', 'boolean'] + coercion: true + from_boolean_false: false + from_boolean_true: true + , ({config}) -> + config.from_boolean_false.should.eql '' + config.from_boolean_true.should.eql '1' + + it 'from integer and number', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_integer': + type: ['string', 'integer'] + coercion: true + 'from_number': + type: ['string', 'number'] + coercion: true + from_integer: 123 + from_number: 1.23 + , ({config}) -> + config.from_integer.should.eql '123' + config.from_number.should.eql '1.23' + + it 'dont conflict with instanceof', -> + nikita + $definitions: + config: + type: 'object' + properties: + from_string: + oneOf: [ + "type": ["string", "number"] + "coercion": true + , + "instanceof": "Buffer" + ] + from_string: 'ok' + , ({config}) -> + config.from_string.should.eql 'ok' + + describe 'boolean', -> + + it 'from string', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_string_empty': + type: ['boolean', 'string'] + coercion: true + 'from_string_filled': + type: ['boolean', 'string'] + coercion: true + from_string_empty: '' + from_string_filled: 'ok' + , ({config}) -> + config.from_string_empty.should.be.false() + config.from_string_filled.should.be.true() + + it 'from number', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_number_0': + type: ['boolean', 'number'] + coercion: true + 'from_number_1': + type: ['boolean', 'number'] + coercion: true + from_number_0: 0 + from_number_1: 1 + , ({config}) -> + config.from_number_0.should.be.false() + config.from_number_1.should.be.true() + + it 'from array (invalid)', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_array': + type: ['boolean', 'array'] + coercion: true + from_array: [0] + , ({config}) -> + config.from_array.should.eql [0] + + describe 'array', -> + + it 'from string and object', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_string_empty': + type: ['array', 'string'] + coercion: true + 'from_string_filled': + type: ['array', 'string'] + coercion: true + 'from_object': + type: ['array', 'object'] + coercion: true + from_string_empty: '' + from_string_filled: 'ok' + from_object: {key: 'value'} + , ({config}) -> + config.from_string_empty.should.eql [''] + config.from_string_filled.should.eql ['ok'] + config.from_object.should.eql [ {key: 'value'} ] + + it 'array shouldnt be altered', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_array': + type: ['array'] + coercion: true + from_array: ['ok'] + , ({config}) -> + config.from_array.should.eql ['ok'] + + it 'forward coerced value to items', -> + nikita + $definitions: + config: + type: 'object' + properties: + 'from_string': + type: ['array', 'string'] + coercion: true + items: + type: [ + "string" + "integer" + ] + filemode: true + from_string: '744' + , ({config}) -> + config.from_string.should.eql [ 0o0744 ] diff --git a/packages/core/test/plugins/tools/schema.coffee b/packages/core/test/plugins/tools/schema.coffee index c329ac53b..768332690 100644 --- a/packages/core/test/plugins/tools/schema.coffee +++ b/packages/core/test/plugins/tools/schema.coffee @@ -44,7 +44,7 @@ describe 'plugins.tools.schema', -> it '`addMetadata` with coercion', -> nikita key: 'value', $meta: 1, (action) -> - action.tools.schema.addMetadata 'meta', type: 'boolean' + action.tools.schema.addMetadata 'meta', type: ['boolean', 'number'], coercion: true action.metadata.definitions = config: type: 'object' diff --git a/packages/core/test/plugins/tools/schema.ref.coffee b/packages/core/test/plugins/tools/schema.ref.coffee index fb99f96b4..755d7ca91 100644 --- a/packages/core/test/plugins/tools/schema.ref.coffee +++ b/packages/core/test/plugins/tools/schema.ref.coffee @@ -72,7 +72,7 @@ describe 'plugins.tools.schema.$ref', -> 'a_target': type: 'object' properties: - 'an_integer': type: 'integer' + 'an_integer': type: ['integer', 'string'], coercion: true 'a_default': type: 'string', default: 'hello' a_source: an_integer: '123' , (action) -> @@ -106,7 +106,7 @@ describe 'plugins.tools.schema.$ref', -> config: { type: 'object', properties: { - an_integer: { type: "integer" }, + an_integer: { type: ["integer", "string"], coercion: true }, a_default: { type: "string", default: "hello" } } } @@ -152,7 +152,7 @@ describe 'plugins.tools.schema.$ref', -> config: type: 'object' properties: - 'an_integer': type: 'integer' + 'an_integer': type: ['integer', 'string'], coercion: true 'a_default': type: 'string', default: 'hello' handler: (->) # Valid schema diff --git a/packages/db/lib/database/schema.json b/packages/db/lib/database/schema.json index 92d883eb0..dd1f47cb9 100644 --- a/packages/db/lib/database/schema.json +++ b/packages/db/lib/database/schema.json @@ -13,6 +13,7 @@ }, "user": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/docker/lib/compose/schema.json b/packages/docker/lib/compose/schema.json index 85661fb95..7192dfc6b 100644 --- a/packages/docker/lib/compose/schema.json +++ b/packages/docker/lib/compose/schema.json @@ -31,6 +31,7 @@ }, "services": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/docker/lib/inspect/schema.json b/packages/docker/lib/inspect/schema.json index 06561c6cd..c3170f309 100644 --- a/packages/docker/lib/inspect/schema.json +++ b/packages/docker/lib/inspect/schema.json @@ -3,14 +3,11 @@ "type": "object", "properties": { "container": { - "type": [ - "array", - "string" - ], + "type": ["array", "string"], "items": { "type": "string" }, - "description": "Name/ID of the container (array of containers not yet implemented)." + "description": "Name/ID of the container(s)." }, "docker": { "$ref": "module://@nikitajs/docker/tools/execute#/definitions/docker" diff --git a/packages/docker/lib/run/schema.json b/packages/docker/lib/run/schema.json index ebdc1465c..2707f629a 100644 --- a/packages/docker/lib/run/schema.json +++ b/packages/docker/lib/run/schema.json @@ -4,6 +4,7 @@ "properties": { "add_host": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -15,6 +16,7 @@ }, "cap_add": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -22,6 +24,7 @@ }, "cap_drop": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -53,6 +56,7 @@ }, "device": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -60,6 +64,7 @@ }, "dns": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -67,6 +72,7 @@ }, "dns_search": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -81,6 +87,7 @@ }, "env": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -88,6 +95,7 @@ }, "env_file": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -95,6 +103,7 @@ }, "expose": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -114,6 +123,7 @@ }, "label": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -125,6 +135,7 @@ }, "link": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -140,6 +151,7 @@ }, "port": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -160,6 +172,7 @@ }, "ulimit": { "type": "array", + "coercion": true, "items": { "type": [ "integer", @@ -170,6 +183,7 @@ }, "volume": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -177,6 +191,7 @@ }, "volumes_from": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/docker/lib/tools/status/schema.json b/packages/docker/lib/tools/status/schema.json index 3dca9df53..3a09a9539 100644 --- a/packages/docker/lib/tools/status/schema.json +++ b/packages/docker/lib/tools/status/schema.json @@ -4,6 +4,7 @@ "properties": { "container": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/docker/lib/volume_create/schema.json b/packages/docker/lib/volume_create/schema.json index 3d55767e1..4145ae862 100644 --- a/packages/docker/lib/volume_create/schema.json +++ b/packages/docker/lib/volume_create/schema.json @@ -11,6 +11,7 @@ }, "label": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -22,6 +23,7 @@ }, "opt": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/file/lib/cache/schema.json b/packages/file/lib/cache/schema.json index 49763a0ac..51c990729 100644 --- a/packages/file/lib/cache/schema.json +++ b/packages/file/lib/cache/schema.json @@ -19,6 +19,7 @@ }, "cookies": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -35,6 +36,7 @@ }, "http_headers": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/file/lib/download/schema.json b/packages/file/lib/download/schema.json index 8d1d37b5d..b314003c2 100644 --- a/packages/file/lib/download/schema.json +++ b/packages/file/lib/download/schema.json @@ -19,6 +19,7 @@ }, "cookies": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -37,6 +38,7 @@ }, "http_headers": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/file/lib/index.js b/packages/file/lib/index.js index 92416094b..a08cff8db 100644 --- a/packages/file/lib/index.js +++ b/packages/file/lib/index.js @@ -8,7 +8,7 @@ export default { handler: async function ({ config, tools: { log } }) { // Content: pass all arguments to function calls const context = arguments[0]; - if(config.source){ + if (config.source) { log("DEBUG", `Source is ${JSON.stringify(config.source)}.`); } log("DEBUG", `Write to destination ${JSON.stringify(config.target)}.`); @@ -68,10 +68,10 @@ export default { // Option "local" force to bypass the ssh // connection, use by the upload function const source = config.source || config.target; - log({ - message: `Force local source is \"${config.local ? "true" : "false"}\"`, - level: "DEBUG", - }); + log( + "DEBUG", + `Force local source is \`${config.local ? "true" : "false"}\`.` + ); const { exists } = await this.fs.base.exists({ $ssh: config.local ? false : undefined, $sudo: config.local ? false : undefined, @@ -85,10 +85,7 @@ export default { } config.content = ""; } - log({ - message: "Reading source", - level: "DEBUG", - }); + log("DEBUG", "Reading source."); ({ data: config.content } = await this.fs.base.readFile({ $ssh: config.local ? false : undefined, $sudo: config.local ? false : undefined, @@ -119,29 +116,18 @@ export default { if (typeof config.target !== "string") { return null; } - log({ - message: "Stat target", - level: "DEBUG", - }); + log("DEBUG", "Stat target."); try { let { stats } = await this.fs.base.lstat({ target: config.target, }); if (utils.stats.isDirectory(stats.mode)) { - throw Error( - "Incoherent situation, target is a directory and there is no source to guess the filename" - ); - // config.target = "#{config.target}/#{path.basename config.source}" - // log message: "Destination is a directory and is now \"config.target\"", level: 'INFO' - // # Destination is the parent directory, let's see if the file exist inside - // {stats} = await @fs.base.stat target: config.target, $relax: 'NIKITA_FS_STAT_TARGET_ENOENT' - // throw Error "Destination is not a file: #{config.target}" unless utils.stats.isFile stats.mode - // log message: "New target exists", level: 'INFO' + throw utils.error("NIKITA_FILE_INCOHERENT_STATE", [ + "Incoherent situation,", + "target is a directory and there is no source to guess the filename", + ]); } else if (utils.stats.isSymbolicLink(stats.mode)) { - log({ - message: "Destination is a symlink", - level: "INFO", - }); + log("INFO", "Destination is a symlink."); if (config.unlink) { await this.fs.base.unlink({ target: config.target, @@ -149,10 +135,7 @@ export default { stats = null; } } else if (utils.stats.isFile(stats.mode)) { - log({ - message: "Destination is a file", - level: "INFO", - }); + log("INFO", "Destination is a file."); } else { throw Error(`Invalid File Type Destination: ${config.target}`); } @@ -183,10 +166,7 @@ export default { }); } if (config.remove_empty_lines) { - log({ - message: "Remove empty lines", - level: "DEBUG", - }); + log("DEBUG", "Remove empty lines."); config.content = config.content.replace( /(\r\n|[\n\r\u0085\u2028\u2029])\s*(\r\n|[\n\r\u0085\u2028\u2029])/g, "$1" @@ -196,10 +176,7 @@ export default { utils.partial(config, log); } if (config.eof) { - log({ - message: "Checking option eof", - level: "DEBUG", - }); + log("DEBUG", "Checking option eof."); if (config.eof === true) { for (let i = 0; i < config.content.length; i++) { const char = config.content[i]; @@ -215,18 +192,13 @@ export default { if (config.eof === true) { config.eof = "\n"; } - log({ - message: `Option eof is true, guessing as ${JSON.stringify( - config.eof - )}`, - level: "INFO", - }); + log( + "INFO", + `Option eof is true, guessing as ${JSON.stringify(config.eof)}.` + ); } if (!utils.string.endsWith(config.content, config.eof)) { - log({ - message: "Add eof", - level: "INFO", - }); + log("INFO", "Add eof."); config.content += config.eof; } } @@ -248,17 +220,12 @@ export default { if (typeof config.diff === "function") { config.diff(text, raw); } - log({ + log("INFO", text, { type: "diff", - message: text, - level: "INFO", }); } if (config.backup && contentChanged) { - log({ - message: "Create backup", - level: "INFO", - }); + log("INFO", "Create backup."); if (config.backup_mode == null) { config.backup_mode = 0o0400; } @@ -273,10 +240,7 @@ export default { } // Call the target with the content when a function if (typeof config.target === "function") { - log({ - message: "Write target with user function", - level: "INFO", - }); + log("INFO", "Write target with user function."); await config.target({ content: config.content, }); diff --git a/packages/file/lib/schema.json b/packages/file/lib/schema.json index d7001ad52..75e946da7 100644 --- a/packages/file/lib/schema.json +++ b/packages/file/lib/schema.json @@ -116,10 +116,7 @@ "description": "Remove empty lines from content" }, "replace": { - "type": [ - "array", - "string" - ], + "type": ["array", "string"], "items": { "type": "string" }, @@ -162,6 +159,7 @@ "write": { "description": "An array containing multiple transformation where a transformation is\nan object accepting the options `from`, `to`, `match` and `replace`.", "type": "array", + "coercion": true, "items": { "type": "object", "properties": { diff --git a/packages/file/lib/types/locale_gen/schema.json b/packages/file/lib/types/locale_gen/schema.json index f213c98ec..66e919f52 100644 --- a/packages/file/lib/types/locale_gen/schema.json +++ b/packages/file/lib/types/locale_gen/schema.json @@ -13,6 +13,7 @@ }, "locales": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/file/lib/types/ssh_authorized_keys/schema.json b/packages/file/lib/types/ssh_authorized_keys/schema.json index bd61f62ef..d878b007d 100644 --- a/packages/file/lib/types/ssh_authorized_keys/schema.json +++ b/packages/file/lib/types/ssh_authorized_keys/schema.json @@ -8,6 +8,7 @@ }, "keys": { "type": "array", + "coercion": true, "description": "Array containing the public keys." }, "merge": { diff --git a/packages/file/lib/utils/partial.js b/packages/file/lib/utils/partial.js index 7e78475e2..5a2ac78aa 100644 --- a/packages/file/lib/utils/partial.js +++ b/packages/file/lib/utils/partial.js @@ -12,10 +12,7 @@ import utils from "@nikitajs/core/utils"; // Utils export default function(config, log) { if(!config.write?.length > 0) return; - log({ - message: "Replacing sections of the file", - level: 'DEBUG' - }); + log("DEBUG", "Replacing sections of the file"); // let orgContent; for (const opts of config.write) { if (opts.match) { @@ -23,40 +20,31 @@ export default function(config, log) { opts.match = opts.replace; } if (typeof opts.match === 'string') { - log({ - message: "Convert match string to regexp", - level: 'DEBUG' - }); + log("DEBUG", "Convert match string to regexp"); } if (typeof opts.match === 'string') { opts.match = RegExp(`${utils.regexp.quote(opts.match)}`, "mg"); } if (!(opts.match instanceof RegExp)) { - throw Error(`Invalid match option, got ${JSON.stringify(opts.match)} instead of a RegExp`); + throw utils.error('NIKITA_PARTIAL_INVALID_MATCH', [ + "Invalid match option,", + `got ${JSON.stringify(opts.match)} instead of a RegExp` + ]); } if (opts.match.test(config.content)) { config.content = config.content.replace(opts.match, opts.replace); - log({ - message: "Match existing partial", - level: 'INFO' - }); + log("INFO", "Match existing partial"); } else if (opts.place_before && typeof opts.replace === 'string') { if (typeof opts.place_before === "string") { opts.place_before = new RegExp(RegExp(`^.*${utils.regexp.quote(opts.place_before)}.*$`, "mg")); } if (opts.place_before instanceof RegExp) { - log({ - message: "Replace with match and place_before regexp", - level: 'DEBUG' - }); + log("DEBUG", "Replace with match and place_before regexp"); let posoffset = 0; const orgContent = config.content; let res; while ((res = opts.place_before.exec(orgContent)) !== null) { - log({ - message: "Before regexp found a match", - level: 'INFO' - }); + log("INFO", "Before regexp found a match"); const pos = posoffset + res.index; //+ res[0].length config.content = config.content.slice(0, pos) + opts.replace + '\n' + config.content.slice(pos); posoffset += opts.replace.length + 1; @@ -64,36 +52,23 @@ export default function(config, log) { break; } } - // place_before = false; // if content } else { - log({ - message: "Forgot how we could get there, test shall say it all", - level: 'DEBUG' - }); + log("DEBUG", "Forgot how we could get there, test shall say it all"); const linebreak = config.content.length === 0 || config.content.substr(config.content.length - 1) === '\n' ? '' : '\n'; config.content = opts.replace + linebreak + config.content; } } else if (opts.append && typeof opts.replace === 'string') { if (typeof opts.append === "string") { - log({ - message: "Convert append string to regexp", - level: 'DEBUG' - }); + log("DEBUG", "Convert append string to regexp"); opts.append = new RegExp(`^.*${utils.regexp.quote(opts.append)}.*$`, 'mg'); } if (opts.append instanceof RegExp) { - log({ - message: "Replace with match and append regexp", - level: 'DEBUG' - }); + log("DEBUG", "Replace with match and append regexp"); let posoffset = 0; const orgContent = config.content; let res; while ((res = opts.append.exec(orgContent)) !== null) { - log({ - message: "Append regexp found a match", - level: 'INFO' - }); + log("INFO", "Append regexp found a match"); const pos = posoffset + res.index + res[0].length; config.content = config.content.slice(0, pos) + '\n' + opts.replace + config.content.slice(pos); posoffset += opts.replace.length + 1; @@ -109,32 +84,20 @@ export default function(config, log) { continue; // Did not match, try callback } } else if (opts.place_before === true) { - log({ - message: "Before is true, need to explain how we could get here", - level: 'INFO' - }); + log("INFO", "Before is true, need to explain how we could get here"); } else if (opts.from || opts.to) { if (opts.from && opts.to) { const from = RegExp(`(^${utils.regexp.quote(opts.from)}$)`, "m").exec(config.content); const to = RegExp(`(^${utils.regexp.quote(opts.to)}$)`, "m").exec(config.content); if ((from != null) && (to == null)) { - log({ - message: "Found 'from' but missing 'to', skip writing", - level: 'WARN' - }); + log("WARN", "Found 'from' but missing 'to', skip writing"); } else if ((from == null) && (to != null)) { - log({ - message: "Missing 'from' but found 'to', skip writing", - level: 'WARN' - }); + log("WARN", "Missing 'from' but found 'to', skip writing"); } else if ((from == null) && (to == null)) { if (opts.append) { config.content += '\n' + opts.from + '\n' + opts.replace + '\n' + opts.to; } else { - log({ - message: "Missing 'from' and 'to' without append, skip writing", - level: 'WARN' - }); + log("WARN", "Missing 'from' and 'to' without append, skip writing"); } } else { config.content = config.content.substr(0, from.index + from[1].length + 1) + opts.replace + '\n' + config.content.substr(to.index); @@ -144,20 +107,14 @@ export default function(config, log) { if (from != null) { config.content = config.content.substr(0, from.index + from[1].length) + '\n' + opts.replace; // TODO: honors append } else { - log({ - message: "Missing 'from', skip writing", - level: 'WARN' - }); + log("WARN", "Missing 'from', skip writing"); } } else if (!opts.from && opts.to) { const to = RegExp(`(^${utils.regexp.quote(opts.to)}$)`, "m").exec(config.content); if (to != null) { config.content = opts.replace + '\n' + config.content.substr(to.index); // TODO: honors append } else { - log({ - message: "Missing 'to', skip writing", - level: 'WARN' - }); + log("WARN", "Missing 'to', skip writing"); } } } diff --git a/packages/file/lib/yaml/schema.json b/packages/file/lib/yaml/schema.json index 8ea340b08..18e5f8850 100644 --- a/packages/file/lib/yaml/schema.json +++ b/packages/file/lib/yaml/schema.json @@ -67,6 +67,7 @@ }, "replace": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/ipa/lib/group/add_member/schema.json b/packages/ipa/lib/group/add_member/schema.json index a8fe4a21c..cb0e9b431 100644 --- a/packages/ipa/lib/group/add_member/schema.json +++ b/packages/ipa/lib/group/add_member/schema.json @@ -11,6 +11,7 @@ "properties": { "user": { "type": "array", + "coercion": true, "minItems": 1, "uniqueItems": true, "items": { diff --git a/packages/ipa/lib/user/find/schema.json b/packages/ipa/lib/user/find/schema.json index c97c8a364..e6bdf4257 100644 --- a/packages/ipa/lib/user/find/schema.json +++ b/packages/ipa/lib/user/find/schema.json @@ -156,60 +156,70 @@ }, "in_group": { "type": "array", + "coercion": true, "items": { "type": "string" } }, "not_in_group": { "type": "array", + "coercion": true, "items": { "type": "string" } }, "in_netgroup": { "type": "array", + "coercion": true, "items": { "type": "string" } }, "not_in_netgroup": { "type": "array", + "coercion": true, "items": { "type": "string" } }, "in_role": { "type": "array", + "coercion": true, "items": { "type": "string" } }, "not_in_role": { "type": "array", + "coercion": true, "items": { "type": "string" } }, "in_hbacrule": { "type": "array", + "coercion": true, "items": { "type": "string" } }, "not_in_hbacrule": { "type": "array", + "coercion": true, "items": { "type": "string" } }, "in_sudorule": { "type": "array", + "coercion": true, "items": { "type": "string" } }, "not_in_sudorule": { "type": "array", + "coercion": true, "items": { "type": "string" } diff --git a/packages/ipa/lib/user/schema.json b/packages/ipa/lib/user/schema.json index 961ad57f1..1a5d5bf45 100644 --- a/packages/ipa/lib/user/schema.json +++ b/packages/ipa/lib/user/schema.json @@ -21,6 +21,7 @@ }, "mail": { "type": "array", + "coercion": true, "minItems": 1, "uniqueItems": true, "items": { diff --git a/packages/java/lib/keystore/remove/schema.json b/packages/java/lib/keystore/remove/schema.json index 7f529d3ac..0206db587 100644 --- a/packages/java/lib/keystore/remove/schema.json +++ b/packages/java/lib/keystore/remove/schema.json @@ -4,6 +4,7 @@ "properties": { "name": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -12,6 +13,7 @@ }, "caname": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/krb5/lib/ktutil/add/schema.json b/packages/krb5/lib/ktutil/add/schema.json index 64c37d6b1..2b5e6cc67 100644 --- a/packages/krb5/lib/ktutil/add/schema.json +++ b/packages/krb5/lib/ktutil/add/schema.json @@ -7,6 +7,7 @@ }, "enctypes": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/ldap/lib/acl/schema.json b/packages/ldap/lib/acl/schema.json index 46873d0e5..91e19191b 100644 --- a/packages/ldap/lib/acl/schema.json +++ b/packages/ldap/lib/acl/schema.json @@ -4,12 +4,14 @@ "properties": { "acls": { "type": "array", + "coercion": true, "description": "In case of multiple acls, regroup \"place_before\", \"to\" and \"by\" as an\narray.", "items": { "type": "object", "properties": { "by": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/ldap/lib/add/schema.json b/packages/ldap/lib/add/schema.json index f082661ce..ac76e6fba 100644 --- a/packages/ldap/lib/add/schema.json +++ b/packages/ldap/lib/add/schema.json @@ -4,6 +4,7 @@ "properties": { "entry": { "type": "array", + "coercion": true, "items": { "type": "object", "properties": { diff --git a/packages/ldap/lib/delete/schema.json b/packages/ldap/lib/delete/schema.json index 51c475f31..73d9cb8e9 100644 --- a/packages/ldap/lib/delete/schema.json +++ b/packages/ldap/lib/delete/schema.json @@ -4,6 +4,7 @@ "properties": { "dn": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/ldap/lib/modify/schema.json b/packages/ldap/lib/modify/schema.json index ceca843be..ac8499e5d 100644 --- a/packages/ldap/lib/modify/schema.json +++ b/packages/ldap/lib/modify/schema.json @@ -5,6 +5,7 @@ "$ref": "module://@nikitajs/ldap/search#/definitions/config", "operations": { "type": "array", + "coercion": true, "items": { "type": "object", "properties": { @@ -19,6 +20,7 @@ }, "attributes": { "type": "array", + "coercion": true, "items": { "type": "object", "properties": { @@ -53,6 +55,7 @@ }, "exclude": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/ldap/lib/search/schema.json b/packages/ldap/lib/search/schema.json index 5d74c1636..b9e18adc9 100644 --- a/packages/ldap/lib/search/schema.json +++ b/packages/ldap/lib/search/schema.json @@ -4,6 +4,7 @@ "properties": { "attributes": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/ldap/lib/user/index.js b/packages/ldap/lib/user/index.js index 6b1e38dbd..65a41908c 100644 --- a/packages/ldap/lib/user/index.js +++ b/packages/ldap/lib/user/index.js @@ -6,12 +6,6 @@ import definitions from "./schema.json" assert { type: "json" }; // Action export default { handler: async function ({ config, tools: { log } }) { - if (!Array.isArray(config.user)) { - // User related config - // Note, very weird, if we don't merge, the user array is traversable but - // the keys map to undefined values. - config.user = [merge(config.user)]; - } let modified = false; for (const user of config.user) { // Add the user @@ -29,9 +23,9 @@ export default { passwd: config.passwd, }); if (added) { - log("WARN", "User added"); + log("WARN", "User added."); } else if (updated) { - log("WARN", "User updated"); + log("WARN", "User updated."); } if (updated || added) { modified = true; diff --git a/packages/ldap/lib/user/schema.json b/packages/ldap/lib/user/schema.json index e33c56f6c..23adafc1a 100644 --- a/packages/ldap/lib/user/schema.json +++ b/packages/ldap/lib/user/schema.json @@ -7,15 +7,12 @@ "description": "Distinguish name storing the \"olcAccess\" property, using the database\naddress (eg: \"olcDatabase={2}bdb,cn=config\")." }, "user": { - "oneOf": [ - { - "type": "object" - }, - { - "type": "array" - } - ], - "description": "User object." + "type": "array", + "coercion": true, + "description": "User object.", + "items": { + "type": "object" + } }, "binddn": { "type": "string", diff --git a/packages/network/lib/http/schema.json b/packages/network/lib/http/schema.json index af497875e..4deb0b26d 100644 --- a/packages/network/lib/http/schema.json +++ b/packages/network/lib/http/schema.json @@ -4,6 +4,7 @@ "properties": { "cookies": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/network/lib/http/wait/schema.json b/packages/network/lib/http/wait/schema.json index 52f91373d..536d7391f 100644 --- a/packages/network/lib/http/wait/schema.json +++ b/packages/network/lib/http/wait/schema.json @@ -9,6 +9,7 @@ }, "status_code": { "type": "array", + "coercion": true, "default": [ "1xx", "2xx", diff --git a/packages/network/lib/tcp/wait/index.js b/packages/network/lib/tcp/wait/index.js index 700a12774..a95c154de 100644 --- a/packages/network/lib/tcp/wait/index.js +++ b/packages/network/lib/tcp/wait/index.js @@ -3,21 +3,11 @@ import dedent from "dedent"; import utils from "@nikitajs/core/utils"; import definitions from "./schema.json" assert { type: "json" }; -// Errors -const errors = { - NIKITA_TCP_WAIT_TIMEOUT: function({config}) { - return utils.error('NIKITA_TCP_WAIT_TIMEOUT', [`timeout reached after ${config.timeout}ms.`]); - } -}; - // Action export default { handler: async function ({ config, tools: { log } }) { if (!config.server?.length) { - log({ - message: "No connection to wait for", - level: "WARN", - }); + log("WARN", "No connection to wait for."); return; } // Validate servers @@ -42,9 +32,11 @@ export default { function compute_md5 { echo $1 | openssl md5 | sed 's/^.* \([a-z0-9]*\)$/\1/g' } - addresses=( ${config.server.map( ({host, port}) => "'" + host + "':'" + port + "'").join(" ")}) + addresses=( ${config.server + .map(({ host, port }) => "'" + host + "':'" + port + "'") + .join(" ")}) timeout=${config.timeout || ""} - md5=\`compute_md5 $${''}{addresses[@]}\` + md5=\`compute_md5 $${""}{addresses[@]}\` randdir="${config.randdir || ""}" if [ -z $randir ]; then # shm and shmfs is also known as tmpfs @@ -71,9 +63,9 @@ export default { fi } function remove_randdir { - for address in "$${''}{addresses[@]}" ; do - host="$${''}{address%%:*}" - port="$${''}{address##*:}" + for address in "$${""}{addresses[@]}" ; do + host="$${""}{address%%:*}" + port="$${""}{address##*:}" rm -f $randdir/\`compute_md5 $host:$port\` done } @@ -117,9 +109,9 @@ export default { } start_time=\`get_time\` # Block until all connections are open - for address in "$${''}{addresses[@]}" ; do - host="$${''}{address%%:*}" - port="$${''}{address##*:}" + for address in "$${""}{addresses[@]}" ; do + host="$${""}{address%%:*}" + port="$${""}{address##*:}" randfile4conn=$randdir/\`compute_md5 $host:$port\` wait_connection $host $port $randfile4conn & done @@ -135,7 +127,9 @@ export default { }); } catch (error) { if (error.exit_code === 2) { - throw errors.NIKITA_TCP_WAIT_TIMEOUT({ config }); + throw utils.error("NIKITA_TCP_WAIT_TIMEOUT", [ + `timeout reached after ${config.timeout}ms.`, + ]); } throw error; } @@ -170,10 +164,14 @@ export default { config.port = [config.port]; } } - return (config.host || []).map(host => (config.port || []).map( port => ({ - host: host, - port: port, - }))).flat(Infinity); + return (config.host || []) + .map((host) => + (config.port || []).map((port) => ({ + host: host, + port: port, + })) + ) + .flat(Infinity); }; const servers = extract_servers(config); if (config.server) { diff --git a/packages/network/lib/tcp/wait/schema.json b/packages/network/lib/tcp/wait/schema.json index c5796a0cc..125e1c93f 100644 --- a/packages/network/lib/tcp/wait/schema.json +++ b/packages/network/lib/tcp/wait/schema.json @@ -4,6 +4,7 @@ "properties": { "host": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -23,8 +24,10 @@ }, "port": { "type": "array", + "coercion": true, "items": { - "type": "integer" + "type": ["integer", "string"], + "coercion": true }, "description": "One or multiple ports, used to build or enrich the 'server' option." }, @@ -33,47 +36,24 @@ "description": "Directory where to write temporary file used internally to store state\ninformation. It default to a temporary location." }, "server": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "properties": { - "host": { - "$ref": "#/definitions/config/properties/host" - }, - "port": { - "$ref": "#/definitions/config/properties/port" - } - } - }, - { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "properties": { - "host": { - "$ref": "#/definitions/config/properties/host" - }, - "port": { - "$ref": "#/definitions/config/properties/port" - } - } - } - ] + "type": "array", + "items": + { + "type": "object", + "properties": { + "host": { + "$ref": "#/definitions/config/properties/host" + }, + "port": { + "$ref": "#/definitions/config/properties/port" } } - ], + }, "description": "One or multiple servers, string must be in the form of\n\"{host}:{port}\", object must have the properties \"host\" and \"port\"." }, "timeout": { - "type": "integer", + "type": ["integer", "string"], + "coercion": true, "description": "Maximum time in millisecond to wait until this action is considered\nto have failed." } } diff --git a/packages/network/test/http/index.coffee b/packages/network/test/http/index.coffee index c0c966f59..8af6d1ad6 100644 --- a/packages/network/test/http/index.coffee +++ b/packages/network/test/http/index.coffee @@ -70,15 +70,17 @@ describe 'network.http', -> describe 'schema', -> - it 'required properties', () -> + it 'required properties url', () -> await nikita.network.http({}, (->)) .should.be.rejectedWith code: 'NIKITA_SCHEMA_VALIDATION_CONFIG' message: "NIKITA_SCHEMA_VALIDATION_CONFIG: one error was found in the configuration of action `network.http`: #/required config must have required property 'url'." + + it 'timeout must be an integer', () -> await nikita.network.http({url: '#', timeout: 'invalid'}, (->)) .should.be.rejectedWith code: 'NIKITA_SCHEMA_VALIDATION_CONFIG' - message: 'NIKITA_SCHEMA_VALIDATION_CONFIG: one error was found in the configuration of action `network.http`: module://@nikitajs/network/tcp/wait#/definitions/config/properties/timeout/type config/timeout must be integer, type is "integer".' + message: 'NIKITA_SCHEMA_VALIDATION_CONFIG: one error was found in the configuration of action `network.http`: module://@nikitajs/network/tcp/wait#/definitions/config/properties/timeout/coercion config/timeout fail to convert string to integer, value is "invalid".' it 'casting', () -> nikita.network.http url: '#', timeout: '1', ({config}) -> diff --git a/packages/network/test/tcp/wait.coffee b/packages/network/test/tcp/wait.coffee index 01b3c7fa8..c4b82d929 100644 --- a/packages/network/test/tcp/wait.coffee +++ b/packages/network/test/tcp/wait.coffee @@ -43,7 +43,7 @@ describe 'network.tcp.wait', -> host: undefined port: 80 $logs.map ({message}) -> message - .should.containEql 'No connection to wait for' + .should.containEql 'No connection to wait for.' it 'option server.host undefined', -> {$logs} = await nikita.network.tcp.wait @@ -51,7 +51,7 @@ describe 'network.tcp.wait', -> { host: undefined, port: 80 } ] $logs.map ({message}) -> message - .should.containEql 'No connection to wait for' + .should.containEql 'No connection to wait for.' it 'option server.port undefined', -> {$logs} = await nikita.network.tcp.wait @@ -59,7 +59,7 @@ describe 'network.tcp.wait', -> { host: 'localhost', port: undefined } ] $logs.map ({message}) -> message - .should.containEql 'No connection to wait for' + .should.containEql 'No connection to wait for.' describe 'run', -> diff --git a/packages/service/lib/install/schema.json b/packages/service/lib/install/schema.json index 06f6a4a4d..1e6a56e33 100644 --- a/packages/service/lib/install/schema.json +++ b/packages/service/lib/install/schema.json @@ -20,11 +20,13 @@ }, "pacman_flags": { "type": "array", + "coercion": true, "default": [], "description": "Additionnal flags passed to the `pacman -S` command." }, "yay_flags": { "type": "array", + "coercion": true, "default": [], "description": "Additionnal flags passed to the `yay -S` command." } diff --git a/packages/service/lib/schema.json b/packages/service/lib/schema.json index a3d0aab02..851ea954a 100644 --- a/packages/service/lib/schema.json +++ b/packages/service/lib/schema.json @@ -31,6 +31,7 @@ }, "state": { "type": "array", + "coercion": true, "items": { "type": "string", "enum": [ diff --git a/packages/system/lib/cgroups/schema.json b/packages/system/lib/cgroups/schema.json index e3a224080..3bd2507d6 100644 --- a/packages/system/lib/cgroups/schema.json +++ b/packages/system/lib/cgroups/schema.json @@ -18,6 +18,7 @@ }, "ignore": { "type": "array", + "coercion": true, "items": { "type": "string" }, @@ -25,6 +26,7 @@ }, "mounts": { "type": "array", + "coercion": true, "description": "List of mount object to add to cgconfig file." }, "merge": { diff --git a/packages/system/lib/info/disks/schema.json b/packages/system/lib/info/disks/schema.json index 36b549ab1..aa15d394e 100644 --- a/packages/system/lib/info/disks/schema.json +++ b/packages/system/lib/info/disks/schema.json @@ -4,6 +4,7 @@ "properties": { "output": { "type": "array", + "coercion": true, "default": [ "source", "fstype", diff --git a/packages/system/lib/mod/index.js b/packages/system/lib/mod/index.js index 8362ad3c8..9c8fb2f46 100644 --- a/packages/system/lib/mod/index.js +++ b/packages/system/lib/mod/index.js @@ -7,14 +7,11 @@ import definitions from "./schema.json" assert { type: "json" }; // Action export default { - handler: async function({metadata, config}) { + handler: async function({config}) { for (const module in config.modules) { const active = config.modules[module]; - let target = config.target; - if (target == null) { - target = `${module}.conf`; - } - target = path.resolve('/etc/modules-load.d', target); + config.target ??= `${module}.conf`; + config.target = path.resolve('/etc/modules-load.d', config.target); await this.execute({ $if: config.load && active, command: dedent` @@ -33,7 +30,7 @@ export default { }); await this.file({ $if: config.persist, - target: target, + target: config.target, match: RegExp(`^${quote(module)}(\\n|$)`, "mg"), replace: active ? `${module}\n` : '', append: true, diff --git a/packages/system/lib/mod/schema.json b/packages/system/lib/mod/schema.json index e14a99996..7a98b7b1e 100644 --- a/packages/system/lib/mod/schema.json +++ b/packages/system/lib/mod/schema.json @@ -3,7 +3,8 @@ "type": "object", "properties": { "load": { - "type": "boolean", + "type": ["boolean", "integer", "string"], + "coercion": true, "default": true, "description": "Load the module with `modprobe`." }, @@ -25,7 +26,8 @@ "description": "Names of the modules." }, "persist": { - "type": "boolean", + "type": ["boolean", "integer", "string"], + "coercion": true, "default": true, "description": "Load the module on startup by placing a file, see `target`." }, diff --git a/packages/system/lib/user/schema.json b/packages/system/lib/user/schema.json index 4afebe46a..6bb33d880 100644 --- a/packages/system/lib/user/schema.json +++ b/packages/system/lib/user/schema.json @@ -16,6 +16,7 @@ }, "groups": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/tools/lib/iptables/schema.json b/packages/tools/lib/iptables/schema.json index 40cb964f4..bc4d8ddf0 100644 --- a/packages/tools/lib/iptables/schema.json +++ b/packages/tools/lib/iptables/schema.json @@ -4,6 +4,7 @@ "properties": { "rules": { "type": "array", + "coercion": true, "items": { "$ref": "#/definitions/rule" }, diff --git a/packages/tools/lib/npm/schema.json b/packages/tools/lib/npm/schema.json index a2c2e50d2..010b6b78c 100644 --- a/packages/tools/lib/npm/schema.json +++ b/packages/tools/lib/npm/schema.json @@ -12,6 +12,7 @@ }, "name": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/tools/lib/npm/uninstall/schema.json b/packages/tools/lib/npm/uninstall/schema.json index 7ec5791e4..b35dfcf55 100644 --- a/packages/tools/lib/npm/uninstall/schema.json +++ b/packages/tools/lib/npm/uninstall/schema.json @@ -7,6 +7,7 @@ }, "name": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/tools/lib/npm/upgrade/schema.json b/packages/tools/lib/npm/upgrade/schema.json index 176ce6a84..354dd2fb9 100644 --- a/packages/tools/lib/npm/upgrade/schema.json +++ b/packages/tools/lib/npm/upgrade/schema.json @@ -12,6 +12,7 @@ }, "name": { "type": "array", + "coercion": true, "items": { "type": "string" }, diff --git a/packages/tools/test/cron/add.coffee b/packages/tools/test/cron/add.coffee index afd551b7d..217f88f10 100644 --- a/packages/tools/test/cron/add.coffee +++ b/packages/tools/test/cron/add.coffee @@ -39,7 +39,7 @@ describe 'tools.cron.add', -> nikita .tools.cron.add command: '/remove/me' - when: true + when: 'invalid' .should.be.rejectedWith code: 'NIKITA_SCHEMA_VALIDATION_CONFIG' message: /#\/definitions\/config\/properties\/when\/pattern config\/when must match pattern/