├── .gitignore ├── examples ├── invalid-docker-app.json └── valid-docker-app.json ├── package.json ├── LICENSE ├── lib ├── tags.js └── schema.js ├── bin └── marathon-validate.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | node_modules 4 | -------------------------------------------------------------------------------- /examples/invalid-docker-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "container": { 3 | "docker": { 4 | "image": "tobilg/mini-webserver", 5 | "network": "BRIDGE", 6 | "portMappings": [ 7 | { "containerPort": 80 } 8 | ] 9 | }, 10 | "type": "DOCKER" 11 | } 12 | } -------------------------------------------------------------------------------- /examples/valid-docker-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "predictionio-server", 3 | "container": { 4 | "docker": { 5 | "image": "tobilg/mini-webserver", 6 | "network": "BRIDGE", 7 | "portMappings": [ 8 | { "containerPort": 80 } 9 | ] 10 | }, 11 | "type": "DOCKER" 12 | }, 13 | "cpus": 0.2, 14 | "mem": 128, 15 | "instances": 1 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marathon-validate", 3 | "version": "0.3.3", 4 | "description": "A tiny command line tool to validate application or group configuration files for Marathon", 5 | "main": "bin/marathon-validate.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "mesos", 11 | "marathon", 12 | "schema", 13 | "validator", 14 | "json", 15 | "application", 16 | "app", 17 | "group" 18 | ], 19 | "author": { 20 | "name": "TobiLG", 21 | "email": "tobilg@gmail.com", 22 | "url": "https://github.com/tobilg" 23 | }, 24 | "bin": { 25 | "marathon-validate": "bin/marathon-validate.js" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/dcos-labs/marathon-validate.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/dcos-labs/marathon-validate/issues" 33 | }, 34 | "license": "MIT", 35 | "dependencies": { 36 | "ajv": "4.11.7", 37 | "commander": "^2.9.0", 38 | "request": "^2.81.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 TobiLG (tobilg@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/tags.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var request = require("request"); 4 | 5 | function Tags () { 6 | 7 | if (!(this instanceof Tags)) { 8 | return new Tags(); 9 | } 10 | 11 | } 12 | 13 | Tags.prototype.getList = function (callback) { 14 | request({ 15 | method: "GET", 16 | uri:"https://api.github.com/repos/mesosphere/marathon/tags?per_page=100", 17 | headers: { 18 | "User-Agent": "marathon-validate" 19 | } 20 | }, function (error, response, body) { 21 | var tags = []; 22 | if (!error && response.statusCode == 200) { 23 | var res = JSON.parse(body); 24 | if (res && Array.isArray(res) && res.length > 0) { 25 | res.forEach(function (tagObj) { 26 | tags.push(" * " + tagObj.name); 27 | }); 28 | callback(null, tags); 29 | } else { 30 | callback(" --> Not OK! Couldn't find any tags!", null); 31 | } 32 | } else { 33 | callback(" --> Not OK! Error occurred when requesting tags!", null); 34 | } 35 | }); 36 | }; 37 | 38 | module.exports = Tags; -------------------------------------------------------------------------------- /bin/marathon-validate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | var pkg = require("../package.json"); 6 | var path = require("path"); 7 | var os = require("os"); 8 | var program = require("commander"); 9 | var Schema = require("../lib/schema"); 10 | var Tags = require("../lib/tags"); 11 | var filePath = ""; 12 | 13 | program 14 | .version(pkg.version) 15 | .usage("[options] ") 16 | .option("-a, --app", "Check an App JSON") 17 | .option("-g, --group", "Check a Group JSON") 18 | .option("-d, --describe ", "Describe a property. Has to be used with either -a (app schema) or -g (group schema)") 19 | .option("-m, --marathon ", "Use schema of specific Marathon version") 20 | .option("-t, --tags", "Get a list of tags for the Marathon project") 21 | .parse(process.argv); 22 | 23 | if (program.describe && (program.app || program.group)) { 24 | var type = ""; 25 | 26 | // Set type 27 | if (program.app) { 28 | type = "app"; 29 | } else if (program.group) { 30 | type = "group"; 31 | } 32 | 33 | var version = program.marathon || "master"; 34 | 35 | // Describe process 36 | var describeSchema = new Schema(type, version, null); 37 | 38 | describeSchema.getDescription(program.describe, function (error, result) { 39 | if (error) { 40 | console.log(error.message); 41 | process.exit(1); 42 | } else if (result) { 43 | console.log(" --> Found " + result.length + " matches for '" + program.describe + "':"); 44 | console.log(result.join("\n")); 45 | process.exit(0); 46 | } 47 | }); 48 | } else if (program.tags) { 49 | // https://api.github.com/repos/mesosphere/marathon/tags?per_page=100 50 | var tags = new Tags(); 51 | tags.getList(function(error, tags) { 52 | if (error) { 53 | console.log(error); 54 | process.exit(1); 55 | } else { 56 | if (tags.length > 0) { 57 | console.log(" --> List of tags:"); 58 | console.log(tags.join(os.EOL)); 59 | } 60 | } 61 | }) 62 | } else { 63 | 64 | if (program.args.length !== 1) { 65 | console.log("Please specify a file to validate!"); 66 | process.exit(1); 67 | } else { 68 | if (path.isAbsolute(program.args[0])) { 69 | filePath = program.args[0]; 70 | } else { 71 | filePath = path.normalize(path.join(process.cwd(), program.args[0])); 72 | } 73 | } 74 | 75 | if (program.app || program.group) { 76 | 77 | var type = ""; 78 | 79 | // Set type 80 | if (program.app) { 81 | type = "app"; 82 | } else if (program.group) { 83 | type = "group"; 84 | } 85 | 86 | var version = program.marathon || "master"; 87 | 88 | var schema = new Schema(type, version, filePath); 89 | 90 | schema.validate(function (error, result) { 91 | if (error) { 92 | console.log(error.message); 93 | process.exit(1); 94 | } else if (result) { 95 | console.log(" --> OK! The file '" + filePath + "' is valid!"); 96 | process.exit(0); 97 | } 98 | }); 99 | 100 | } else { 101 | console.log("Please either use the --app or --group flags!"); 102 | process.exit(1); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Warning: This tool is not up-to-date with the current marathon version** 2 | 3 | # marathon-validate 4 | 5 | A tiny command line tool to validate application or group configuration files for Marathon and DC/OS. 6 | 7 | ## Purpose 8 | 9 | If you're running a Mesos or DC/OS cluster and build custom applications for if, most of the time you'll have to create either a Marathon app definition JSON file, or a group definition JSON file. 10 | 11 | As the structure of these files can get a little complicated, `marathon-validate` was created to be able to do a quick sanity check of these files from the command line. 12 | 13 | Therefore, `marathon-validate` will use the [JSON schema files](https://github.com/mesosphere/marathon/tree/master/docs/docs/rest-api/public/api/v2/schema) contained in the Marathon GitHub project to validate the input file against. 14 | 15 | ## Installation 16 | 17 | To be able to use `marathon-validate`, you need to have Node.js (and NPM) installed on your system. Then you can use 18 | 19 | ```bash 20 | npm install -g marathon-validate 21 | ``` 22 | 23 | to install it globally. You can verify the correct installation by issuing 24 | 25 | ```bash 26 | $ marathon-validate --version 27 | 0.3.3 28 | ``` 29 | 30 | ## Usage 31 | 32 | ``` 33 | $ marathon-validate --help 34 | 35 | Usage: marathon-validate [options] 36 | 37 | Options: 38 | 39 | -h, --help output usage information 40 | -V, --version output the version number 41 | -a, --app Check an App JSON 42 | -g, --group Check a Group JSON 43 | -d, --describe Describe a property. Has to be used with either -a (app schema) or -g (group schema) 44 | -m, --marathon Use schema of specific Marathon version 45 | -t, --tags Get a list of tags for the Marathon project 46 | ``` 47 | 48 | ### Validate apps and groups 49 | 50 | If you want validate your `application.json` file in the current folder against the `master` version of the JSON schema, you can do a 51 | 52 | ```bash 53 | $ marathon-validate -a application.json 54 | ``` 55 | 56 | To validate your `application.json` against a specific release version (e.g. `v1.3.6`), you can use 57 | 58 | ```bash 59 | $ marathon-validate -a -m v1.3.6 application.json 60 | ``` 61 | 62 | This should work with all `tags` from the [Marathon project](https://api.github.com/repos/mesosphere/marathon/tags). 63 | 64 | ### Query tags 65 | 66 | You can also get the list of tags like this (output is shortened): 67 | 68 | ```bash 69 | $ marathon-validate -t 70 | --> List of tags: 71 | * v1.5.0-SNAPSHOT 72 | * v1.4.3 73 | * v1.4.2 74 | * v1.4.2-snapshot5 75 | * v1.4.2-snapshot4 76 | * v1.4.2-snapshot3 77 | * v1.4.2-snapshot2 78 | * v1.4.2-SNAPSHOT1 79 | * v1.4.1 80 | * v1.4.0 81 | ... 82 | ``` 83 | 84 | ### Search for field description 85 | 86 | You can search the JSON schema for a field's description like this (in this example, the `type` field): 87 | 88 | ```bash 89 | $ marathon-validate -a -d type 90 | --> Loading remote schema: https://raw.githubusercontent.com/mesosphere/marathon/master/docs/docs/rest-api/public/api/v2/schema/AppDefinition.json 91 | --> Found 2 matches for 'type': 92 | * '.container.type': Container engine type. Supported engine types at the moment are DOCKER and MESOS. 93 | * '.container.volumes.persistent.type': The type of mesos disk resource to use; defaults to root 94 | ``` 95 | 96 | By using the `-a` or `-g` flags, you can specify if you want to query the app or group JSON schema. 97 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); 4 | var request = require("request"); 5 | var Ajv = require('ajv'); 6 | 7 | function Schema (type, version, fileToCheck) { 8 | 9 | if (!(this instanceof Schema)) { 10 | return new Schema(type, version, fileToCheck); 11 | } 12 | 13 | var self = this; 14 | self.baseUrl = "https://raw.githubusercontent.com/mesosphere/marathon/%%VERSION%%/docs/docs/rest-api/public/api/v2/schema"; 15 | self.allowedTypes = { 16 | "app": "AppDefinition.json", 17 | "group": "Group.json" 18 | }; 19 | 20 | self.options = {}; 21 | self.options.type = type || null; 22 | self.options.version = version || "master"; 23 | self.options.fileToCheck = fileToCheck || null; 24 | 25 | if (fileToCheck) { 26 | self.ajv = new Ajv({ extendRefs: true, loadSchema: self.getRemoteSchema.bind(self) }); 27 | } 28 | 29 | } 30 | 31 | Schema.prototype.validate = function (callback) { 32 | 33 | var self = this; 34 | 35 | function parseValidationError (errors) { 36 | var errorMessages = []; 37 | errorMessages.push(" --> Not OK! The following errors occured during validation: "); 38 | if (errors) { 39 | errors.forEach(function (error) { 40 | errorMessages.push(" * '" + error.keyword + "' error. The definition of '" + error.dataPath + "' " + error.message); 41 | }); 42 | } 43 | return errorMessages.join("\n"); 44 | } 45 | 46 | var url = self.baseUrl.replace("%%VERSION%%", self.options.version) + "/" + self.allowedTypes[self.options.type]; 47 | 48 | self.getRemoteSchema(url, function (error, schemaContents) { 49 | if (error) { 50 | 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); 51 | } else { 52 | self.getFile(function (error, fileContents) { 53 | if (error) { 54 | callback(error, null); 55 | } else { 56 | self.ajv.compileAsync(schemaContents, function (err, validate) { 57 | if (err) { 58 | callback(err, null); 59 | } 60 | var valid = validate(fileContents); 61 | if (!valid) { 62 | callback({ message: parseValidationError(validate.errors) }, null); 63 | } else { 64 | callback(null, valid); 65 | } 66 | }); 67 | } 68 | }); 69 | } 70 | }); 71 | 72 | }; 73 | 74 | Schema.prototype.getDescription = function (propertyName, callback) { 75 | 76 | var self = this; 77 | var propertyMap = {}; 78 | 79 | // Iterate of the schema and return the unique property names and their paths and descriptions 80 | function iterate(obj, stack) { 81 | for (var property in obj) { 82 | if (obj.hasOwnProperty(property)) { 83 | if (typeof obj[property] === "object") { 84 | if (obj[property].hasOwnProperty("properties")) { 85 | iterate(obj[property].properties, stack + '.' + property); 86 | } else if (obj[property].hasOwnProperty("items") && obj[property].items.hasOwnProperty("properties")) { 87 | iterate(obj[property].items.properties, stack + '.' + property); 88 | } else { 89 | if (!propertyMap.hasOwnProperty(property.toLowerCase())) { 90 | propertyMap[property.toLowerCase()] = {}; 91 | } 92 | propertyMap[property.toLowerCase()][stack + "." + property] = { 93 | name: property, 94 | description: (obj[property].description ? obj[property].description.replace(/\r?\n|\r/g, "") : "") 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | function searchProperty(propertyName) { 103 | var matches = []; 104 | Object.getOwnPropertyNames(propertyMap).forEach(function (property) { 105 | if (property === propertyName) { 106 | Object.getOwnPropertyNames(propertyMap[property]).forEach(function (pathResult) { 107 | matches.push(" * '" + pathResult + "': " + propertyMap[property][pathResult].description); 108 | }); 109 | } 110 | }); 111 | return matches; 112 | } 113 | 114 | var url = self.baseUrl.replace("%%VERSION%%", self.options.version) + "/" + self.allowedTypes[self.options.type]; 115 | 116 | self.getRemoteSchema(url, function (error, schemaContents) { 117 | if (error) { 118 | 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); 119 | } else { 120 | if (schemaContents.properties && Object.getOwnPropertyNames(schemaContents.properties).length > 0) { 121 | // Iterate over schema 122 | iterate(schemaContents.properties, ""); 123 | // Search for property 124 | var searchResults = searchProperty(propertyName.toLowerCase()); 125 | // Evaluate results 126 | if (searchResults.length === 0) { 127 | callback({ message: " --> Not OK! No matching property found!" }, null); 128 | } else { 129 | callback(null, searchResults); 130 | } 131 | } else { 132 | callback({ message: " --> Not OK! There was a problem while getting the schema file!" }, null); 133 | } 134 | } 135 | }); 136 | 137 | }; 138 | 139 | Schema.prototype.getFile = function (callback) { 140 | 141 | var self = this; 142 | 143 | try { 144 | var file = fs.statSync(self.options.fileToCheck); 145 | try { 146 | var contents = JSON.parse(fs.readFileSync(self.options.fileToCheck, "utf8")); 147 | callback(null, contents); 148 | } catch (error) { 149 | callback({ message: " --> Not OK! The file couldn't be parsed as JSON!" }, null); 150 | } 151 | } catch (error) { 152 | callback({ message: " --> Not OK! The file '" + self.options.fileToCheck + "' couldn't be found!" }, null); 153 | } 154 | 155 | }; 156 | 157 | Schema.prototype.getRemoteSchema = function (url, callback) { 158 | 159 | var self = this; 160 | 161 | if (self.options.version !== "master" && url.indexOf("master") > -1) { 162 | url = url.replace("master", self.options.version); 163 | } 164 | 165 | var options = { 166 | method: "GET", 167 | url: url 168 | }; 169 | 170 | console.log(" --> Loading remote schema: " + options.url); 171 | 172 | request(options, function (error, response, body) { 173 | if (!error && response.statusCode === 200) { 174 | callback(null, JSON.parse(body)); 175 | } else { 176 | callback({ message: error, statusCode: response.statusCode }, null); 177 | } 178 | }); 179 | 180 | }; 181 | 182 | module.exports = Schema; 183 | --------------------------------------------------------------------------------