UNPKG

pshregistry-parser

Version:

Helper for library for accessing image data from the Upsun Registry and generating configuration files.

428 lines (397 loc) 14.5 kB
'use strict'; let fs = require('fs'); let psh = require('./images.js'); let yml = require('js-yaml'); class InvalidRegistryError extends SyntaxError { constructor (message) { super(message); SyntaxError.captureStackTrace( this, this.constructor ); this.name = 'InvalidRegistryError'; this.message = message; } } class NotValidImageError extends ReferenceError { constructor (message) { super(message); ReferenceError.captureStackTrace( this, this.constructor ); this.name = 'NotValidImageError'; this.message = message; } } /** * Main RegistryParser class for accessing Registry image data and writing configuration files. * * Example configuration YAML files can be written using: * * registry.write(); * * Writes three configuration files for every runtime (.platform.app.yaml) and * six files for each service (.platform.app.yaml & .platform/services.yaml) in * three formats - heavily commented full files, un-commented full files, single * line snippets. * * Alternatively you can pass a single image to write() * * registry.write("elasticsearch"); * * and only the six files for Elasticsearch will be generated/updated. * * Some definitions * * - Special Cases: There are certain images (i.e. Varnish) that require additional * information in their template strings - whether that be comments or additional properties * specific to that Image's configuration. In these cases, a new class for that * Image included in images.js. * Note: these Images contain their own repositories, and are therfore included in a manual/generated Registry JSON. * * - Additional Types: Another case that must be handled by ConfigGenerator are those * images that contain multiple valid 'types' for a single image repository (i.e., 'mysql' * is a valid 'type' for MariaDB, but there will not be a 'mysql' object in the Registry as * a 'images/mysql' repository does not exist). * In these cases, since we want valid configuration files for every type, additional types * are included in the 'additionalTypes' property. Each new 'type' describes it's 'baseImage' * (the repository that type is pulled from), and includes any other properties different * from that baseImage in an 'diff' object that will be used to generate its example files. * * * @param {string} registrySource * The location of the local registry.json file. * @param {string} saveDir * The location to save the generated files (default=null). Default will create * and `examples` directory in the location of registrySource and save files there. */ class RegistryParser { constructor(registrySource, saveDir=null) { this.additionalTypes = { "redis-persistent": { "baseImage": "redis", "diff": { "type": "redis-persistent", "name": "Persistent Redis", "disk": true, "docs": { "relationship_name": "redisdata", "service_name": "data", "url": "\/add-services\/redis.html" } } } } this.ignoreTypesInTable = { "mysql": true }; this.saveLocations = this.deriveSaveLocations(saveDir, registrySource); this.services = {}; this.runtimes = {}; this.images = this.makeImageInstances(registrySource); this.contentFileName = "content.json"; this.tableFileNames = { "runtimes": "runtimes_supported.md", "services": "services_supported.md" }; this.directoryContent = { "commented": [], "full": [], "snippet": [], "tables": [] } } /** * Returns a reformatted image string capitalized to match a potential Image class. * * @param {string} imageType * A Registry JSON image key. * @return {string} * Capitalized image key to match potential Image class. */ reformatImageTypeToPotentialClassName(imageType) { let targetClass = imageType.charAt(0).toUpperCase() + imageType.slice(1); let arr = targetClass.split("-"); if (arr.length > 1) { targetClass = arr[0] + arr[1].charAt(0).toUpperCase() + arr[1].slice(1); } else { targetClass = arr[0]; } return targetClass; } /** * Returns Image class that will be instantiated for a Registry image type. * * @param {string} imageType * A Registry JSON image key. * @param {object} registry * A Registry JSON object. * @return {string} * The Image class name - Runtime, Service, or a Special Case class. */ getClassNameForImage(imageType, registry) { let reformattedImageType = this.reformatImageTypeToPotentialClassName(imageType); let targetClass = "Service"; if (psh[reformattedImageType]) { targetClass = reformattedImageType; } else if (registry[imageType].runtime) { targetClass = "Runtime"; } return targetClass; } /** * Duplicates Registry data in a YAML file. * * @param {string} registrySource * A Registry JSON source file name. */ writeRegistryYAML(registrySource) { const registryName = registrySource.split("."); let registryData = this.parseRegistry(registrySource); fs.writeFileSync(`${registryName[0]}.yaml`, yml.dump(registryData, {noRefs:true}), function (err) { if (err) throw err; }); } /** * Expands registry images into properties of this instance, returning a list of those properties. * * @param {object} registrySource * A Registry JSON source. * @return {array} * A list of available Images to facilitate iteration. */ makeImageInstances(registrySource) { // Parses the Registry. let registry = this.parseRegistry(registrySource); // For now, make an identical copy of registry data in a YAML file. this.writeRegistryYAML(registrySource); // Creates a list of Images. let images = {}; // For each image, create an instance that is accessible as a property. for (let currentImage in registry) { const className = this.getClassNameForImage(currentImage, registry); images[currentImage] = new psh[className](registry[currentImage]); // Filter individual images by imageGroup as the instances are created. if (images[currentImage].runtime) { this.runtimes[currentImage] = images[currentImage]; } else { this.services[currentImage] = images[currentImage]; } } return images; } /** * Adds additional type cases to the parse Registry object. * * @param {object} registryData * A Registry object. * @return {object} * An updated Registry object that includes the additional types. */ updateRegistryWithAdditionalTypes(registryData) { // Add each additional type to the Registry object using it's equivalent base image. for (let newType in this.additionalTypes) { registryData[newType] = Object.assign({}, registryData[this.additionalTypes[newType].baseImage]); // Update each property for the new type that is different from its base image. for (let property in this.additionalTypes[newType].diff) { registryData[newType][property] = this.additionalTypes[newType].diff[property]; } } return registryData; } /** * Parses the Registry JSON. * * @param {string} sourceFile * JSON source filename for the Registry. * @return {object} * A Registry JSON object, updated with additional types where defined in * this.additionalTypes. */ parseRegistry(sourceFile) { let unparsedRegistry = fs.readFileSync(sourceFile); let registryData; // Parse it. try { registryData = this.updateRegistryWithAdditionalTypes(JSON.parse(unparsedRegistry)); } catch (exc) { throw new InvalidRegistryError(exc); } // Validate that all entries have a type property for (let imageKey in registryData) { if (!Object.hasOwn(registryData[imageKey], 'type')) { throw new InvalidRegistryError(`Registry entry "${imageKey}" is missing required property: type`); } } return registryData; } /** * Ensures that the derived save locations exist, creating them if they don't. * */ ensureSaveLocations(saveDir) { // Check if each save directory exists, and create them if they don't. if (!fs.existsSync(saveDir)){ fs.mkdirSync(saveDir); } if (!fs.existsSync(saveDir + 'examples/')){ fs.mkdirSync(saveDir + 'examples/'); } if (!fs.existsSync(saveDir + 'examples/commented/')){ fs.mkdirSync(saveDir + 'examples/commented/'); } if (!fs.existsSync(saveDir + 'examples/full/')){ fs.mkdirSync(saveDir + 'examples/full/'); } if (!fs.existsSync(saveDir + 'examples/snippet/')){ fs.mkdirSync(saveDir + 'examples/snippet/'); } if (!fs.existsSync(saveDir.split('/').slice(0, -1).join('/') + "/" + 'tables/')){ fs.mkdirSync(saveDir.split('/').slice(0, -1).join('/') + "/" + 'tables/'); } } /** * Derives the example file save locations from either user input or the default location. * */ deriveSaveLocations(saveDir, registrySource) { // If no saveDir is defined, use the directory where the registry.json is located. if (saveDir == null) { let split = registrySource.split('/'); split.splice(-1,1); saveDir = split.join('/') + '/'; } // Check if each save directory exists, create them if they don't. this.ensureSaveLocations(saveDir); return { "commented": saveDir + 'examples/commented/', "full": saveDir + 'examples/full/', "snippet": saveDir + 'examples/snippet/', "tables": saveDir + 'tables/' }; } /** * Writes a file. * * @param {string} fileType * Type of template content and file save location (e.g. commented). * @param {string} filename * Filename with save location prefix included. * @param {string} content * The string that will be written to the generated file. */ writeFile(fileType, filename, content) { let fullFile = `${this.saveLocations[fileType]}${filename}`; fs.writeFileSync(fullFile, content, function (err) { if (err) throw err; }); if (!filename.includes(this.contentFileName)) { this.directoryContent[fileType].push(filename); } } /** * Constructs the supported versions table (markdown) for the give image group. * * @param {string} imageGroup * Image group, can be either "runtimes" or "services". * @return {string} * A markdown formatted supported versions table. */ makeSupportedVersionsTable(imageGroup) { // Construct the table. let table = ""; if (imageGroup === "runtimes") { table += "| **Language** | **`runtime`** | **Supported `version`** |"; } else if (imageGroup === "services") { table += "| **Service** | **`type`** | **Supported `version`** |"; } table += "\n|----------------------------------|---------------|-------------------------|"; // Add rows for each image to the table. let images = this[imageGroup]; Object.keys(images) // Filter out the additional types. .filter(image => !(Object.keys(this.additionalTypes).includes(image))) // Filter out types where versions do not match (mysql/mariadb). .filter(image => !(Object.keys(this.ignoreTypesInTable).includes(image))) // Sort alphabetically by type. .sort() // Create and append the row. .forEach((image, index) => { table += `\n| [${images[image].name}](${images[image].docs.url}) | \`${images[image].type}\` | ${images[image].supportedString} |`; }); return table; } /** * Generates the supported versions table (markdown) files for runtime and service images. * */ makeSupportedVersionsTables() { for (let tableFileName in this.tableFileNames) { // Make the content string. let tableContent = this.makeSupportedVersionsTable(tableFileName); // Write the file. this.writeFile("tables", this.tableFileNames[tableFileName], tableContent); } } /** * Generates all of the example YAML files for a given image. * * @param {object} image * A Service, Runtime, or Special Case object. */ generateYAMLFiles(image) { // Write example files for app, services, and routes configurations. for(let majorConfig in image.config) { // Write example files for commented, full, and snippets. for(let fileType in image.config[majorConfig]) { // Don't write files that contain empty strings. let content = image.config[majorConfig][fileType]; // Write the content. if (content) { let filename; // Handle app subkey yaml file names. if (majorConfig !== "app" && majorConfig !== "services" && majorConfig !== "routes") { filename = `${image.type}.${majorConfig}.app.yaml`; // Handle all other main app and services yaml file names. } else { filename = `${image.type}.${majorConfig}.yaml`; } // Write the file. this.writeFile(fileType, filename, content); } } } } /** * Primary method for writing all configuration files. * * @param {string} currentImage * The image key of the Registry object. If the image is undefined, * the function will generate files for every image in the Registry. */ write(currentImage) { if (currentImage === undefined) { for (let image in this.images) { this.write(image); } this.makeSupportedVersionsTables(); this.generateContentJSONFiles(); } else { if (!this.images.hasOwnProperty(currentImage)) { throw new NotValidImageError(`Files cannot be generated for "${currentImage}", because it's not a valid image in the Upsun registry.`); } this.generateYAMLFiles(this.images[currentImage]); } } /** * Generates the content.json files for each examples subdirectory. * */ generateContentJSONFiles() { // Generate content.json for each fileType/saveLocation. for (let location in this.directoryContent) { let subdirContent = JSON.stringify({"files": this.directoryContent[location]}, null, 2); this.writeFile(location, this.contentFileName, subdirContent); } } } module.exports = { RegistryParser, NotValidImageError, InvalidRegistryError };