Skip to content

Commit

Permalink
improved error messages and simplify the UX for calling apply/validat…
Browse files Browse the repository at this point in the history
…eConfig
  • Loading branch information
aflanagan committed May 7, 2024
1 parent 792e1d9 commit 23879ce
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 68 deletions.
44 changes: 21 additions & 23 deletions lib/cronitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,45 +72,42 @@ function Cronitor(apiKey, config = {}) {
};


this.applyConfig = async function(rollback = false) {
if (!this.config) throw new Errors.ConfigError('Must call cronitor.readConfig({path: \'path/to/config\'}) before calling applyConfig().');

// try:
// monitors = Monitor.put(monitors=config, rollback=rollback, format=YAML)
// job_count = len(monitors.get('jobs', []))
// check_count = len(monitors.get('checks', []))
// heartbeat_count = len(monitors.get('heartbeats', []))
// total_count = sum([job_count, check_count, heartbeat_count])
// logger.info('{} monitor{} {}'.format(total_count, 's' if total_count != 1 else '', 'validated.' if rollback else 'synced.',))
// return True
// except (yaml.YAMLError, ConfigValidationError, APIValidationError, APIError, AuthenticationError) as e:
// logger.error(e)
// return False
this.applyConfig = async function({ path = this.path, rollback = false } = {}) {
if (!path) throw new Errors.ConfigError('Must include a path to config file e.g. cronitor.applyConfig({path: \'./cronitor.yaml\'})');

try {
config = await this.readConfig({ path, output: true});
} catch (err) {
console.error('Error reading config:', err);
return false
}

try {
console.log('CONFIG', this.config)
await Monitor.put(this.config, {rollback, format: Monitor.requestType.YAML});
await Monitor.put(config, {rollback, format: Monitor.requestType.YAML});
console.log(`Cronitor config ${rollback ? 'validated' : 'applied'} successfully.`)
return true
} catch (err) {
console.log(`Error applying config: ${err}`);
console.error(`Error applying config: ${err}`);
return false
}
};

this.validateConfig = async () => {
return this.applyConfig(true);
this.validateConfig = async ({ path = this.path} = {}) => {
return this.applyConfig({ path, rollback: true });
};

this.readConfig = async function({path = null, output = false}={}) {
path = path || this.path;

if (!path) throw new Errors.ConfigError('Must include a path to config file e.g. cronitor.readConfig({path: \'./cronitor.yaml\'})');
if (!this.path) this.path = path;

try {
this.config = await fs.readFile(path, 'utf8');
let configFile = await fs.readFile(path, 'utf8')
this.config = yaml.load(configFile);
if (output) return this.config;
return true
} catch (err) {
throw new Errors.ConfigError("Error reading Cronitor config file: ", err);
console.error('Error reading Cronitor config file:', err);
return false
}
};

Expand Down Expand Up @@ -150,6 +147,7 @@ function Cronitor(apiKey, config = {}) {
job.start();
};
} else {
console.log(lib.CronJob)
throw new Errors.ConfigError(`Unsupported library ${lib.name}`);
}

Expand Down
44 changes: 25 additions & 19 deletions lib/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,43 @@ class Monitor {
};
}

static async put(data, {rollback = false, format = Monitor.requestType.JSON} = {}) {
// if (typeof data == 'object') {
// if (!Array.isArray(data)) {
// data = [data];
// }
// } else {
// throw new Errors.MonitorNotCreated('Invalid monitor data.');
// }

console.log("PUT ARGS", data, rollback, type)
static async put(data, {rollback = false, format = Monitor.requestType.JSON} = {}) {

let payload
if (format === Monitor.requestType.YAML) {
payload = yaml.dump({...data, rollback});
console.log("THE PAYLOAD", payload)
this._api.axios.defaults.headers['Content-Type'] = 'application/yaml';
} else {
payload = { monitors: data, rollback }
return this.putYaml(yaml.dump({...data, rollback}))

}

// if a user passed a single monitor object, wrap it in an array
if (!Array.isArray(data)) {
data = [data];
}

try {
const resp = await this._api.axios.put(this._api.monitorUrl(), payload);
const resp = await this._api.axios.put(this._api.monitorUrl(), {monitors: data, rollback});
const monitors = resp.data.monitors.map((_m) => {
const m = new Monitor(_m.key);
m.data = _m;
return m;
});
return monitors.length > 1 ? monitors : monitors[0];
return monitors.length > 1 ? monitors : monitors[0];
} catch (err) {
throw new Errors.MonitorNotCreated(err.message);
}
}

static async putYaml(payload) {
try {
const resp = await this._api.axios.put(
this._api.monitorUrl(),
payload,
{ headers: {'Content-Type': 'application/yaml'} }
);
return yaml.load(resp.data);
} catch (err) {
throw new Errors.MonitorNotCreated(err.message);
}

}

constructor(key) {
Expand Down
122 changes: 96 additions & 26 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,44 @@ describe('Config Parser', () => {
context('validateConfig', () => {
afterEach(() => {
sinon.restore();
cronitor.path = null;
});
it('should call Monitor.put with a YAML payload and rollback: true', async () => {
const stub = sinon.stub(cronitor.Monitor, 'put');
await cronitor.readConfig({path: './test/cronitor.yaml'});
await cronitor.validateConfig();
expect(stub).to.be.calledWith(sinon.match.string, { rollback: true, format: Monitor.requestType.YAML});
await cronitor.validateConfig({path: './test/cronitor.yaml'});
expect(stub).to.be.calledWith(sinon.match.object, { rollback: true, format: Monitor.requestType.YAML});
});

it('should raise an exception if no path is provided', async () => {
try {
await cronitor.validateConfig();
expect.fail('Should have raised an error');
} catch (err) {
expect(err.message).to.eq('Must include a path to config file e.g. cronitor.applyConfig({path: \'./cronitor.yaml\'})');
}
})
});

context('applyConfig', () => {
afterEach(async () => {
sinon.restore();
cronitor.path = null;
});

it('should call Monitor.put with array of monitors and rollback: false', async () => {
const stub = sinon.stub(cronitor.Monitor, 'put');
await cronitor.readConfig({path: './test/cronitor.yaml'});
await cronitor.applyConfig();
expect(stub).to.be.calledWith(sinon.match.string, { rollback: false, format: Monitor.requestType.YAML});
await cronitor.applyConfig({path: './test/cronitor.yaml'});
expect(stub).to.be.calledWith(sinon.match.object, { rollback: false, format: Monitor.requestType.YAML});
});

it('should raise an exception if no path is provided', async () => {
try {
await cronitor.validateConfig();
expect.fail('Should have raised an error');
} catch (err) {
expect(err.message).to.eq('Must include a path to config file e.g. cronitor.applyConfig({path: \'./cronitor.yaml\'})');
}
})
});


Expand Down Expand Up @@ -85,6 +103,16 @@ describe('Config Parser', () => {
console.error('Failed to read the file:', err);
}
});

it('should allow a group to be specified', async () => {
const stub = sinon.stub(cronitor._api.axios, 'get');
const dummyData = await fs.readFile('./test/cronitor.yaml', 'utf8');
stub.resolves({data: dummyData})
const resp = await cronitor.generateConfig({path: './cronitor-test.yaml', group: 'test-group'});

expect(stub).to.be.calledWith('https://cronitor.io/api/monitors.yaml?group=test-group');
expect(resp).to.be.true;
});
});
});

Expand Down Expand Up @@ -275,28 +303,70 @@ describe('Event', () => {
});
});

// describe('test wrap cron', () => {
// it('should load the node-cron library and define the wrapper function', () => {
// cronitor.wraps(require('node-cron'));
describe.skip('test wrap cron', () => {
it('should load the node-cron library and define the wrapper function', () => {
cronitor.wraps(require('node-cron'));

// cronitor.schedule('everyMinuteJob', '* * * * *', () => {
// return new Promise(function(resolve) {
// setTimeout(() => {
// console.log('running node-cron every min');
// resolve('i ran for 10 seconds');
// }, 3000);
// });
// });
cronitor.schedule('everyMinuteJob', '* * * * *', () => {
console.log('running node-cron every min');
});

// });
});

// it('should load the NodeCron library and define the wrapper function', () => {
// cronitor.wraps(require('cron'));
it('should load the NodeCron library and define the wrapper function', () => {
cronitor.wraps(require('cron'));

// cronitor.schedule('everyMinuteJob', '* * * * *', () => {
// console.log('running cron every min');
// return 'i ran for 10 seconds';
// });
cronitor.schedule('everyMinuteJob', '* * * * *', () => {
console.log('running cron every min');
return 'i ran for 10 seconds';
});

// });
// });
});
});

describe.skip('functional test YAML API', () => {
const cronitor = require('../lib/cronitor')('ADD_YOUR_API_KEY')

it('should read a config file and validate it', async () => {
const validated = await cronitor.validateConfig({path: './test/cronitor.yaml'});
expect(validated).to.be.true;
});

it('should read a config file and apply it', async () => {
const applied = await cronitor.applyConfig({path: './test/cronitor.yaml'});
expect(applied).to.be.true;

// clean up if this runs against prod
const config = await cronitor.readConfig({path: './test/cronitor.yaml', output: true});
keys = Object.keys(config).map((k) => Object.keys(config[k])).flat();
keys.map(async (k) => {
const monitor = new cronitor.Monitor(k);
await monitor.delete()
});
});

it('should create a monitor from an object', async () => {
const monitor = await Monitor.put({
key: 'test-monitor',
name: 'Test Monitor',
type: 'job',
})
expect(monitor).to.be.instanceOf(Monitor);
await monitor.delete();
});

it('should create monitors from a list', async () => {
const monitors = await Monitor.put([{
key: 'test-monitor',
name: 'Test Monitor',
type: 'job',
},{
key: 'test-monitor-1',
name: 'Test Monitor1',
type: 'job',
}])
expect(monitors).to.be.instanceOf(Array);
expect(monitors.length).to.eq(2);
monitors.forEach(async (m) => await m.delete());
});
})

0 comments on commit 23879ce

Please sign in to comment.