Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add command resource #88

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: add command resource
  • Loading branch information
sadams committed Jan 12, 2021

Verified

This commit was signed with the committer’s verified signature.
uk-bolly uk-bolly
commit 1e634d1f2fad9fd1a6b6a484170facccaa3733f6
2 changes: 2 additions & 0 deletions bin/usage.txt
Original file line number Diff line number Diff line change
@@ -30,6 +30,8 @@ Description:
For http over socket, use http://unix:SOCK_PATH:URL_PATH
like http://unix:/path/to/sock:/foo/bar or
http-get://unix:/path/to/sock:/foo/bar
command: - Executes an arbitrary command. ex: "command:ls /tmp"
Succeeds if the exit code of the command is 0

Standard Options:

31 changes: 30 additions & 1 deletion lib/wait-on.js
Original file line number Diff line number Diff line change
@@ -11,14 +11,16 @@ const axiosHttpAdapter = require('axios/lib/adapters/http');
const { isBoolean, isEmpty, negate, noop, once, partial, pick, zip } = require('lodash/fp');
const { NEVER, combineLatest, from, merge, throwError, timer } = require('rxjs');
const { distinctUntilChanged, map, mergeMap, scan, startWith, take, takeWhile } = require('rxjs/operators');
const childProcess = require('child_process');

// force http adapter for axios, otherwise if using jest/jsdom xhr might
// be used and it logs all errors polluting the logs
const axios = axiosPkg.create({ adapter: axiosHttpAdapter });
const isNotABoolean = negate(isBoolean);
const isNotEmpty = negate(isEmpty);
const fstat = promisify(fs.stat);
const PREFIX_RE = /^((https?-get|https?|tcp|socket|file):)(.+)$/;
const exec = promisify(childProcess.exec);
const PREFIX_RE = /^((https?-get|https?|tcp|socket|file|command):)(.+)$/;
const HOST_PORT_RE = /^(([^:]*):)?(\d+)$/;
const HTTP_GET_RE = /^https?-get:/;
const HTTP_UNIX_RE = /^http:\/\/unix:([^:]+):([^:]+)$/;
@@ -186,6 +188,8 @@ function createResource$(deps, resource) {
return createHTTP$(deps, resource);
case 'tcp:':
return createTCP$(deps, resource);
case 'command:':
return createCommand$(deps, resource);
case 'socket:':
return createSocket$(deps, resource);
default:
@@ -388,6 +392,31 @@ async function socketExists(output, socketPath) {
});
}

function createCommand$({ validatedOpts: { delay, interval, reverse, simultaneous }, output }, resource) {
const command = extractPath(resource);
const checkFn = reverse ? negateAsync(commandPasses) : commandPasses;
return timer(delay, interval).pipe(
mergeMap(() => {
output(`executing command "${command}" ...`);
return from(checkFn(output, command));
}, simultaneous),
startWith(false),
distinctUntilChanged(),
take(2)
);
}

async function commandPasses(output, command) {
try {
const {stdout} = await exec(command);
output(` Command "${command}" success. stdout: "${stdout}"`);
return true;
} catch(e) {
output(` Command error: "${e.message}"`);
return false;
}
}

function negateAsync(asyncFn) {
return async function (...args) {
return !(await asyncFn(...args));
150 changes: 150 additions & 0 deletions test/api.mocha.js
Original file line number Diff line number Diff line change
@@ -765,4 +765,154 @@ describe('api', function () {
});
});
});

describe('command', function () {
describe('normal mode', function () {
it('should succeed when command passes', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const fileExists1 = path.resolve(dirPath, 'exists1')
const fileExists2 = path.resolve(dirPath, 'exists2')
const opts = {
resources: [
`command:ls ${fileExists1}`,
`command:ls ${fileExists2}`
],
};
fs.writeFileSync(fileExists1, 'data1');
fs.writeFileSync(fileExists2, 'data2');

waitOn(opts)
.then(function () {
done();
})
.catch(function (err) {
done(err);
});
});
});

it('should succeed when a command passes later', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const fileWillExist1 = path.resolve(dirPath, 'willexist1')
const fileWillExist2 = path.resolve(dirPath, 'willexist2')
const opts = {
resources: [
`command:ls ${fileWillExist1}`,
`command:ls ${fileWillExist2}`
],
};
setTimeout(function () {
fs.writeFileSync(fileWillExist1, 'data1');
fs.writeFileSync(fileWillExist2, 'data2');
}, 300);

waitOn(opts)
.then(function () {
done();
})
.catch(function (err) {
done(err);
});
});
});

it('should timeout when command fails', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const notExists = path.resolve(dirPath, 'NOTexists')
const opts = {
resources: [
`command:ls ${notExists}`
],
timout: 1000,
};

waitOn(opts)
.then(function () {
done(new Error('Should not be resolved'));
})
.catch(function (err) {
expect(err).toExist();
done();
});
});
});
});

describe('reverse mode', function () {
it('should succeed when command fails in reverse mode', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const notExists = path.resolve(dirPath, 'NOTexists')
const opts = {
resources: [
`command:ls ${notExists}`,
],
reverse: true
};

waitOn(opts)
.then(function () {
done();
})
.catch(function (err) {
done(err);
});
});
});

it('should succeed when command fails later in reverse mode', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const willBeDeleted1 = path.resolve(dirPath, 'deleteme1')
const willBeDeleted2 = path.resolve(dirPath, 'deleteme2')
const opts = {
resources: [
`command:ls ${willBeDeleted1}`,
`command:ls ${willBeDeleted2}`
],
};
fs.writeFileSync(willBeDeleted1, 'data1');
fs.writeFileSync(willBeDeleted2, 'data2');
setTimeout(function () {
fs.unlinkSync(willBeDeleted1);
fs.unlinkSync(willBeDeleted2);
}, 300);

waitOn(opts)
.then(function () {
done();
})
.catch(function (err) {
done(err);
});
});
});

it('should timeout when command passes in reverse mode', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const exists = path.resolve(dirPath, 'exists1')
const opts = {
resources: [
`command:ls ${exists}`
],
reverse: true,
timeout: 1000,
};
fs.writeFileSync(exists, 'data1');
waitOn(opts)
.then(function () {
done(new Error('Should not be resolved'));
})
.catch(function (err) {
expect(err).toExist();
done();
});
});
});
});
});
});
128 changes: 128 additions & 0 deletions test/cli.mocha.js
Original file line number Diff line number Diff line change
@@ -533,4 +533,132 @@ describe('cli', function () {
done();
});
});

describe('command', function () {
describe('normal mode', function () {
it('should succeed when command passes', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const fileExists1 = path.resolve(dirPath, 'exists1')
const fileExists2 = path.resolve(dirPath, 'exists2')
const opts = {
resources: [
`command:ls ${fileExists1}`,
`command:ls ${fileExists2}`
],
};
fs.writeFileSync(fileExists1, 'data1');
fs.writeFileSync(fileExists2, 'data2');

execCLI(opts.resources.concat(FAST_OPTS), {}).on('exit', function (code) {
expect(code).toBe(0);
done();
});
});
});

it('should succeed when a command passes later', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const fileWillExist1 = path.resolve(dirPath, 'willexist1')
const fileWillExist2 = path.resolve(dirPath, 'willexist2')
const opts = {
resources: [
`command:ls ${fileWillExist1}`,
`command:ls ${fileWillExist2}`
],
};
setTimeout(function () {
fs.writeFileSync(fileWillExist1, 'data1');
fs.writeFileSync(fileWillExist2, 'data2');
}, 300);

execCLI(opts.resources.concat(FAST_OPTS), {}).on('exit', function (code) {
expect(code).toBe(0);
done();
});
});
});

it('should timeout when command fails', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const notExists = path.resolve(dirPath, 'NOTexists')
const opts = {
resources: [
`command:ls ${notExists}`
],
};

execCLI(opts.resources.concat(FAST_OPTS), {}).on('exit', function (code) {
expect(code).toNotBe(0);
done();
});
});
});
});

describe('reverse mode', function () {
const REV_OPTS = FAST_OPTS.concat(['-r']);

it('should succeed when command fails in reverse mode', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const notExists = path.resolve(dirPath, 'NOTexists')
const opts = {
resources: [
`command:ls ${notExists}`,
],
};

execCLI(opts.resources.concat(REV_OPTS), {}).on('exit', function (code) {
expect(code).toBe(0);
done();
});
});
});

it('should succeed when command fails later in reverse mode', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const willBeDeleted1 = path.resolve(dirPath, 'deleteme1')
const willBeDeleted2 = path.resolve(dirPath, 'deleteme2')
const opts = {
resources: [
`command:ls ${willBeDeleted1}`,
`command:ls ${willBeDeleted2}`
],
};
fs.writeFileSync(willBeDeleted1, 'data1');
fs.writeFileSync(willBeDeleted2, 'data2');
setTimeout(function () {
fs.unlinkSync(willBeDeleted1);
fs.unlinkSync(willBeDeleted2);
}, 300);

execCLI(opts.resources.concat(REV_OPTS), {}).on('exit', function (code) {
expect(code).toBe(0);
done();
});
});
});

it('should timeout when command passes in reverse mode', function (done) {
temp.mkdir({}, function (err, dirPath) {
if (err) return done(err);
const exists = path.resolve(dirPath, 'exists1')
const opts = {
resources: [
`command:ls ${exists}`
],
};
fs.writeFileSync(exists, 'data1');
execCLI(opts.resources.concat(REV_OPTS), {}).on('exit', function (code) {
expect(code).toNotBe(0);
done();
});
});
});
});
});
});