pshregistry-parser
Version:
Helper for library for accessing image data from the Platform.sh Registry and generating configuration files.
418 lines (390 loc) • 14.2 kB
JavaScript
;
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) {
// Read the Registry source.
let unparsedRegistry = fs.readFileSync(sourceFile);
// Parse it.
try {
return this.updateRegistryWithAdditionalTypes(JSON.parse(unparsedRegistry));
} catch (exc) {
throw new InvalidRegistryError(exc);
}
}
/**
* 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 Platform.sh 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
};