marathon-validate
Version:
A tiny command line tool to validate application or group configuration files for Marathon
183 lines (150 loc) • 6.79 kB
JavaScript
;
var fs = require("fs");
var request = require("request");
var Ajv = require('ajv');
function Schema (type, version, fileToCheck) {
if (!(this instanceof Schema)) {
return new Schema(type, version, fileToCheck);
}
var self = this;
self.baseUrl = "https://raw.githubusercontent.com/mesosphere/marathon/%%VERSION%%/docs/docs/rest-api/public/api/v2/schema";
self.allowedTypes = {
"app": "AppDefinition.json",
"group": "Group.json"
};
self.options = {};
self.options.type = type || null;
self.options.version = version || "master";
self.options.fileToCheck = fileToCheck || null;
if (fileToCheck) {
self.ajv = new Ajv({ extendRefs: true, loadSchema: self.getRemoteSchema.bind(self) });
}
}
Schema.prototype.validate = function (callback) {
var self = this;
function parseValidationError (errors) {
var errorMessages = [];
errorMessages.push(" --> Not OK! The following errors occured during validation: ");
if (errors) {
errors.forEach(function (error) {
errorMessages.push(" * '" + error.keyword + "' error. The definition of '" + error.dataPath + "' " + error.message);
});
}
return errorMessages.join("\n");
}
var url = self.baseUrl.replace("%%VERSION%%", self.options.version) + "/" + self.allowedTypes[self.options.type];
self.getRemoteSchema(url, function (error, schemaContents) {
if (error) {
callback({ message: (error.statusCode && error.statusCode === 404 ? " --> Not OK! The Marathon version '" + self.options.version + "' doesn't seem to exist! Please check..." : "An error occured while downloading the schema...") }, null);
} else {
self.getFile(function (error, fileContents) {
if (error) {
callback(error, null);
} else {
self.ajv.compileAsync(schemaContents, function (err, validate) {
if (err) {
callback(err, null);
}
var valid = validate(fileContents);
if (!valid) {
callback({ message: parseValidationError(validate.errors) }, null);
} else {
callback(null, valid);
}
});
}
});
}
});
};
Schema.prototype.getDescription = function (propertyName, callback) {
var self = this;
var propertyMap = {};
// Iterate of the schema and return the unique property names and their paths and descriptions
function iterate(obj, stack) {
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
if (typeof obj[property] === "object") {
if (obj[property].hasOwnProperty("properties")) {
iterate(obj[property].properties, stack + '.' + property);
} else if (obj[property].hasOwnProperty("items") && obj[property].items.hasOwnProperty("properties")) {
iterate(obj[property].items.properties, stack + '.' + property);
} else {
if (!propertyMap.hasOwnProperty(property.toLowerCase())) {
propertyMap[property.toLowerCase()] = {};
}
propertyMap[property.toLowerCase()][stack + "." + property] = {
name: property,
description: (obj[property].description ? obj[property].description.replace(/\r?\n|\r/g, "") : "")
}
}
}
}
}
}
function searchProperty(propertyName) {
var matches = [];
Object.getOwnPropertyNames(propertyMap).forEach(function (property) {
if (property === propertyName) {
Object.getOwnPropertyNames(propertyMap[property]).forEach(function (pathResult) {
matches.push(" * '" + pathResult + "': " + propertyMap[property][pathResult].description);
});
}
});
return matches;
}
var url = self.baseUrl.replace("%%VERSION%%", self.options.version) + "/" + self.allowedTypes[self.options.type];
self.getRemoteSchema(url, function (error, schemaContents) {
if (error) {
callback({ message: (error.statusCode && error.statusCode === 404 ? " --> Not OK! The Marathon version '" + self.options.version + "' doesn't seem to exist! Please check..." : "An error occured while downloading the schema...") }, null);
} else {
if (schemaContents.properties && Object.getOwnPropertyNames(schemaContents.properties).length > 0) {
// Iterate over schema
iterate(schemaContents.properties, "");
// Search for property
var searchResults = searchProperty(propertyName.toLowerCase());
// Evaluate results
if (searchResults.length === 0) {
callback({ message: " --> Not OK! No matching property found!" }, null);
} else {
callback(null, searchResults);
}
} else {
callback({ message: " --> Not OK! There was a problem while getting the schema file!" }, null);
}
}
});
};
Schema.prototype.getFile = function (callback) {
var self = this;
try {
var file = fs.statSync(self.options.fileToCheck);
try {
var contents = JSON.parse(fs.readFileSync(self.options.fileToCheck, "utf8"));
callback(null, contents);
} catch (error) {
callback({ message: " --> Not OK! The file couldn't be parsed as JSON!" }, null);
}
} catch (error) {
callback({ message: " --> Not OK! The file '" + self.options.fileToCheck + "' couldn't be found!" }, null);
}
};
Schema.prototype.getRemoteSchema = function (url, callback) {
var self = this;
if (self.options.version !== "master" && url.indexOf("master") > -1) {
url = url.replace("master", self.options.version);
}
var options = {
method: "GET",
url: url
};
console.log(" --> Loading remote schema: " + options.url);
request(options, function (error, response, body) {
if (!error && response.statusCode === 200) {
callback(null, JSON.parse(body));
} else {
callback({ message: error, statusCode: response.statusCode }, null);
}
});
};
module.exports = Schema;