pshregistry-parser
Version:
Helper for library for accessing image data from the Upsun Registry and generating configuration files.
765 lines (688 loc) • 29.1 kB
JavaScript
'use strict';
const semver = require('semver');
/**
* Utility method to get the value of a nested object key that may not exist.
*
* @param obj
* The object to read.
* @param args
* A variadic list of the properties to traverse.
* @return {*}
*/
function getNested(obj, ...args) {
return args.reduce((obj, level) => obj && obj[level], obj);
}
/**
* Main Image class for each image in the Upsun Flex/Fixed Registry.
*
* It defines example configuration files in 'config' with empty content, which
* are filled out via the child Runtime, Service, and special cases classes.
*
* Each Image can return an HTML unordered list and raw string for both "supported"
* and "deprecated" lifecycles of the Image that can be used in the documentation.
*
*/
class Image {
constructor(data) {
this.makeDataAccessibleAsProperties(data);
this.recommended = this.getRecommendedVersion();
this.supportedHTML = this.lifecycleHTML("supported");
this.supportedString = this.lifecycleString("supported");
this.deprecatedHTML = this.lifecycleHTML("deprecated");
this.deprecatedString = this.lifecycleString("deprecated");
this.config = {
"app": {
"commented": "",
"full": "",
"snippet": "",
},
"web" : {
"commented": "",
"full": "",
"snippet": "",
},
"hooks": {
"commented": "",
"full": "",
"snippet": "",
},
"build": {
"commented": "",
"full": "",
"snippet": "",
},
"dependencies": {
"commented": "",
"full": "",
"snippet": "",
},
"routes": {
"commented": "",
"full": "",
"snippet": "",
},
"services" : {
"commented": "",
"full": "",
"snippet": "",
}
}
}
/**
* Expands registry data into properties of this instance.
*
* @param {object} data
* Registry data object for a given image.
*/
makeDataAccessibleAsProperties(data) {
for(let dataProperty in data) {
if (dataProperty === "versions") {
this["supported"] = getNested(data, "versions", "supported") || {};
this["deprecated"] = getNested(data, "versions", "deprecated") || {};
} else {
this[dataProperty] = data[dataProperty];
}
}
}
/**
* Generates the HTML unordered list of either the supported or deprecated versions for this instance.
*
* @param {string} lifecycle
* The lifecycle the user is interested in for the Image ("supported", "deprecated").
* @return {string}
* The HTML unordered list of Image versions.
*/
lifecycleHTML(lifecycle) {
let unorderedList = "";
// Only build the string if the Image contains versions for the lifecycle.
if (this[lifecycle].length !== 0) {
unorderedList += "<ul>";
for(let version of this[lifecycle]) {
unorderedList += `<li>${version}</li>`;
}
unorderedList += "</ul>";
}
return unorderedList;
}
/**
* Generates a basic list of either the supported or deprecated versions for the instance.
*
* @param {string} lifecycle
* The lifecycle the user is interested in for the Image ("supported", "deprecated").
* @return {string}
* The list of Image versions.
*/
lifecycleString(lifecycle) {
let fullString = "";
// Only build the string if the Image contains versions for the lifecycle.
if (this[lifecycle].length !== 0) {
fullString += this[lifecycle].join(", ");
}
return fullString;
}
/**
* Determines the recommended/newest version index of a supported version array.
*
* @return {string}
* The index for the recommended/newest image version.
*/
getRecommendedVersion() {
// Use suported versions, unless there are none
// Then check for Dedicated versions
// If nothing supported, use deprecated versions
const getVersionArray = () => {
if (this.supported.length > 0) return this.supported.slice()
if (this["versions-dedicated"]?.supported.length > 0) return this["versions-dedicated"].supported.slice()
return this.deprecated.slice()
}
const versionArray = getVersionArray();
// Sort to find the most recent version
const sortedVersionArray = versionArray.sort((a,b) => semver.lt(semver.coerce(a),semver.coerce(b)) ? 1 : -1)
return sortedVersionArray[0]
}
}
/**
* The Runtime class extends the Image class for runtime images.
*
* It contains all of the same methods described above for an Image, but updates
* the template strings for the returned .platform.app.yaml files that describe
* the runtime type.
*
*/
class Runtime extends Image {
constructor(data) {
super(data);
this.config.app.snippet = `
type: '${this.type}:${this.recommended}'
`.trim();
this.config.app.full = `
type: '${this.type}:${this.recommended}'
`.trim();
this.config.app.commented = `
# The runtime the application uses. The 'type' key defines the base container
# image that will be used to run the application. There is a separate base
# container image for each primary language for the application,
# in multiple versions. Check the ${this.name} documentation
# (https:\/\/docs.upsun.com/anchors/fixed/languages/${this.type}/#supported-versions)
# to find the supported versions for the '${this.type}' type.
type: '${this.type}:${this.recommended}'
`.trim();
this.config.web.snippet = this.makeWeb();
this.config.web.full = this.makeWeb();
this.config.web.commented = this.makeWeb(true);
this.config.hooks.snippet = this.makeHooks();
this.config.hooks.full = this.makeHooks();
this.config.hooks.commented = this.makeHooks(true);
this.config.build.snippet = this.makeBuild();
this.config.build.full = this.makeBuild();
this.config.build.commented = this.makeBuild(true);
this.config.dependencies.snippet = this.makeDependencies();
this.config.dependencies.full = this.makeDependencies();
this.config.dependencies.commented = this.makeDependencies(true);
}
/**
* Creates content string for .platform.app.yaml 'build' attribute.
*
* @param {boolean} commented
* Defaults false. true returns heavily commented content string.
* @return {string}
* Content string for the to be generated yaml file.
*/
makeBuild (commented=false) {
let comment_build = `
# 'build' defines what happens when building the application. Its only
# property is 'flavor', which specifies a default set of build tasks to run.
# Flavors are language-specific.
# DOCS: https://docs.upsun.com/anchors/fixed/app/reference/build/
`.trim();
let content = ``;
if (this.docs.hasOwnProperty('build')) {
content = `
${commented ? comment_build : ""}
build:
flavor: ${this.docs.build.flavor}
`.trim().replace(/^\s*\n/gm, "");
}
return content;
}
/**
* Creates content string for .platform.app.yaml 'dependencies' attribute.
*
* @param {boolean} commented
* Defaults false. true returns heavily commented content string.
* @return {string}
* Content string for the to be generated yaml file.
*/
makeDependencies (commented=false) {
let comment_dependencies = `
# It is also possible to install additional system-level dependencies as part of
# the build process. These can be installed before the build hook runs using the
# native package manager for several web-focused languages. When specifying
# dependencies for each language, use the appropriate block for that language.
# DOCS: https://docs.upsun.com/anchors/fixed/app/reference/dependencies/
`.trim();
let dependencies = ``;
let content = ``;
if (this.docs.hasOwnProperty('dependencies')) {
for (let language in this.docs.dependencies) {
dependencies += ` ${language}:`;
for (let dep in this.docs.dependencies[language]) {
dependencies += `\n ${dep}: ${this.docs.dependencies[language][dep]}`;
}
}
content = `
${commented ? comment_dependencies : ""}
dependencies:
${dependencies}
`.trim().replace(/^\s*\n/gm, "");
}
return content;
}
/**
* Creates content string for .platform.app.yaml 'hooks' attribute.
*
* @param {boolean} commented
* Defaults false. true returns heavily commented content string.
* @return {string}
* Content string for the to be generated yaml file.
*/
makeHooks (commented=false) {
let comment_hooks = `# Upsun Fixed supports three "hooks", or points in the deployment of a new version of an
# application that you can inject a custom script into. Each runs at a different stage
# of the process.
# DOCS: https://docs.upsun.com/anchors/fixed/app/hooks/compare/\n`;
let comment_build_hook = `
# The 'build' hook is run after the build flavor (if any). The file system is fully writable,
# but no services (such as a database) are available nor are any persistent file mounts, as the
# application has not yet been deployed.
# DOCS: https://docs.upsun.com/anchors/fixed/app/hooks/compare/build/`;
let comment_deploy_hook = `
# The 'deploy' hook is run after the application container has been started, but before it
# has started accepting requests. Services are accesible at this stage. The file system is
# read-only from this stage onward excluding defined mounts.
# DOCS: https://docs.upsun.com/anchors/fixed/app/hooks/compare/deploy/`;
let content = ``;
if (this.docs.hasOwnProperty('hooks')) {
let build_hook_commands = ``;
if (this.docs.hooks.hasOwnProperty('build')) {
for (let line in this.docs.hooks.build) {
if(line == 0) {
build_hook_commands += `${this.docs.hooks.build[line]}`;
} else {
build_hook_commands += `\n ${this.docs.hooks.build[line]}`;
}
}
}
let deploy_hook_commands = ``;
if (this.docs.hooks.hasOwnProperty('deploy')) {
for (let line in this.docs.hooks.deploy) {
if(line == 0) {
deploy_hook_commands += `${this.docs.hooks.deploy[line]}`;
} else {
deploy_hook_commands += `\n ${this.docs.hooks.deploy[line]}`;
}
}
}
content = `
${(commented && this.docs.hasOwnProperty('hooks')) ? comment_hooks : ""}
hooks:
${(commented && this.docs.hooks.hasOwnProperty('build')) ? comment_build_hook : ""}
${this.docs.hooks.hasOwnProperty('build') ? "build:" : ""} ${this.docs.hooks.hasOwnProperty('build')
? build_hook_commands : ""}
${(commented && this.docs.hooks.hasOwnProperty('deploy')) ? comment_deploy_hook : ""}
${this.docs.hooks.hasOwnProperty('deploy') ? "deploy:" : ""} ${this.docs.hooks.hasOwnProperty('deploy')
? deploy_hook_commands : ""}
`.trim().replace(/^\s*\n/gm, "");
}
return content;
}
/**
* Creates content string for .platform.app.yaml 'web' attribute.
*
* @param {boolean} commented
* Defaults false. true returns heavily commented content string.
* @return {string}
* Content string for the to be generated yaml file.
*/
makeWeb (commented=false) {
let comment_web = `
# The 'web' key defines a single web instance container running a single web server
# process (currently Nginx), behind which runs your application. It configures
# the web server, including what requests should be served directly
# (such as static files) and which should be passed to your application.
# DOCS: https://docs.upsun.com/anchors/fixed/app/reference/web/
`.trim();
let comment_upstream = `
# 'upstream' specifies how the front server will connect to your application
# (the process started by 'commands.start').
# DOCS: https://docs.upsun.com/anchors/fixed/app/reference/web/upstream/
`.trim();
let comment_socket_family = `
# Describes whether your application will listen on a Unix socket
# ('unix') or a TCP socket ('tcp'). Default: 'tcp'.
# DOCS: https://docs.upsun.com/anchors/fixed/app/reference/web/upstream/socket-family/
`.trim();
let comment_protocol = `
# Specifies whether your application is going to receive incoming requests
# over HTTP ('http') or FastCGI ('fastcgi'). Default is runtime-dependent.
`.trim();
let comment_commands = `
# The 'commands' key defines the command to launch the application.
# DOCS: https://docs.upsun.com/anchors/fixed/app/reference/web/commands/
`.trim();
let comment_start = `
# The 'start' key specifies the command to use to launch your application.
# If the command specified by the 'start' key terminates it will
# be restarted automatically.
`.trim();
let comment_locations = `
# 'locations' allows you to control how the application container responds
# to incoming requests at a very fine-grained level.
# DOCS: https://docs.upsun.com/anchors/fixed/app/reference/web/locations/
`.trim();
let comment_root = `
# The public directory of the app, relative to its root.
`.trim();
let comment_passthru = `
# Whether to forward disallowed and missing resources from this
# location to the application. Can be 'true', 'false', or an absolute
# URI path (as in PHP, when it is typically the front controller).
`.trim();
let comment_expires = `
# How long to allow static assets from this location to be cached.
`.trim();
let comment_allow = `
# Whether to allow serving files which don't match a rule. Default 'true'.
`.trim();
let content = ``;
if (this.docs.hasOwnProperty('web')) {
content = `
${commented ? comment_web : ""}
web:
${(commented && this.docs.web.hasOwnProperty('upstream')) ? comment_upstream : ""}
${this.docs.web.hasOwnProperty('upstream') ? "upstream:" : ""}
${(commented && this.docs.web.hasOwnProperty('upstream')
&& this.docs.web.upstream.hasOwnProperty('socket_family'))
? comment_socket_family : ""}
${(this.docs.web.hasOwnProperty('upstream')
&& this.docs.web.upstream.hasOwnProperty('socket_family'))
? "socket_family: " + this.docs.web.upstream.socket_family : ""}
${(commented && this.docs.web.hasOwnProperty('upstream')
&& this.docs.web.upstream.hasOwnProperty('protocol'))
? comment_protocol : ""}
${(this.docs.web.hasOwnProperty('upstream')
&& this.docs.web.upstream.hasOwnProperty('protocol'))
? "protocol: " + this.docs.web.upstream.protocol : ""}
${(commented && this.docs.web.hasOwnProperty('commands')) ? comment_commands : ""}
${this.docs.web.hasOwnProperty('commands') ? "commands:" : ""}
${(commented && this.docs.web.hasOwnProperty('commands')
&& this.docs.web.commands.hasOwnProperty('start')) ? comment_start : ""}
${(this.docs.web.hasOwnProperty('commands')
&& this.docs.web.commands.hasOwnProperty('start'))
? "start: " + this.docs.web.commands.start : ""}
${(commented && this.docs.web.hasOwnProperty('locations')) ? comment_locations : ""}
${this.docs.web.hasOwnProperty('locations') ? "locations:" : ""}
${this.docs.web.hasOwnProperty('locations') ? "/:" : ""}
${(commented && this.docs.web.hasOwnProperty('locations')
&& this.docs.web.locations["/"].hasOwnProperty('root')) ? comment_root : ""}
${(this.docs.web.hasOwnProperty('locations')
&& this.docs.web.locations["/"].hasOwnProperty('root'))
? "root: " + this.docs.web.locations["/"].root : ""}
${(commented && this.docs.web.hasOwnProperty('locations')
&& this.docs.web.locations["/"].hasOwnProperty('passthru')) ? comment_passthru : ""}
${(this.docs.web.hasOwnProperty('locations')
&& this.docs.web.locations["/"].hasOwnProperty('passthru'))
? "passthru: " + this.docs.web.locations["/"].passthru : ""}
${(commented && this.docs.web.hasOwnProperty('locations')
&& this.docs.web.locations["/"].hasOwnProperty('expires')) ? comment_expires : ""}
${(this.docs.web.hasOwnProperty('locations')
&& this.docs.web.locations["/"].hasOwnProperty('expires'))
? "expires: " + this.docs.web.locations["/"].expires : ""}
${(commented && this.docs.web.hasOwnProperty('locations')
&& this.docs.web.locations["/"].hasOwnProperty('allow')) ? comment_allow : ""}
${(this.docs.web.hasOwnProperty('locations')
&& this.docs.web.locations["/"].hasOwnProperty('allow'))
? "allow: " + this.docs.web.locations["/"].allow : ""}
`.trim().replace(/^\s*\n/gm, "");
}
return content;
}
}
/**
* The Service class extends the Image class for service images.
*
* It contains all of the same methods described above for an Image, but updates
* the template strings for the returned .platform.app.yaml files (so that they
* describe relationship blocks). It also adds template strings to the returned
* services.yaml files, to reflect service definitions.
*
*/
class Service extends Image {
constructor(data) {
super(data);
let defaults = {
"min_disk_size": 256
};
this.config.app.snippet = ` ${this.docs.relationship_name}: "${this.docs.service_name}:${this.endpoint}"`;
this.config.app.full = `
relationships:
${this.docs.relationship_name}: "${this.docs.service_name}:${this.endpoint}"
`.trim();
this.config.app.commented = `
# The relationships block defines how services are mapped within your application.
relationships:
# The relationship is specified in the form 'service_name:endpoint_name'.
# The 'service_name' is the name of the service given in '.platform.services.yaml'.
# The 'endpoint_name' is the exposed functionality of the service to use. In most
# cases this is simply the same as the service 'type', but there are a few exceptions.
${this.docs.relationship_name}: "${this.docs.service_name}:${this.endpoint}"
`.trim();
let uncommentedTemplate = `
${this.docs.service_name}:
type: ${this.type}:${this.recommended}
`.trim();
let commentedTemplate = `
# The name given to the ${this.name} service (lowercase alphanumeric only).
${this.docs.service_name}:
# The type of your service (${this.type}), which uses the format
# 'type:version'. Be sure to consult the ${this.name} documentation
# (https://docs.upsun.com/anchors/fixed/services/${this.type}/#supported-versions)
# when choosing a version. If you specify a version number which is not available,
# the CLI will return an error.
type: ${this.type}:${this.recommended}
`.trim();
let commentedTemplateDisk = "# The disk attribute is the size of the persistent disk (in MB) allocated to the service.";
this.config.services.snippet = this.addDiskString(uncommentedTemplate, defaults.min_disk_size);
this.config.services.full = this.addDiskString(uncommentedTemplate, defaults.min_disk_size);
this.config.services.commented = this.addDiskString(commentedTemplate, defaults.min_disk_size, commentedTemplateDisk);
}
/**
* Appends a Service file's template string with `disk` if the Service requires it.
*
* @param {string} templateString
* The existing template string for a Service file.
* @param {bool} commented
* Defines whether the resulting file should contain the commented version of the
* `disk` definition.
* @return {string}
* The updated template string with `disk` definition appended if appropriate.
*/
addDiskString(templateString, defaultDisk, commentedTemplate=null) {
let diskString = templateString;
if (this.disk) {
let currentDisk = defaultDisk;
if (this.min_disk_size != null) {
currentDisk = this.min_disk_size;
}
if (commentedTemplate != null) {
diskString = `
${templateString}
${commentedTemplate}
disk: ${currentDisk}
`.trim();
} else {
diskString = `
${templateString}
disk: ${currentDisk}
`.trim();
}
}
return diskString;
}
}
/**
* The NetworkStorage class is a special case of a Service.
*
* It modifies modifying the template strings generated for NetworkStorage:
*
* - Network storage does not require defining a relationship block, but mounts must be
* defined on every application container that wants to use it. This class modifies the
* .platform.app.yaml template strings to define those mounts.
*
*/
class NetworkStorage extends Service {
constructor(data) {
super(data);
this.config.app.snippet = ` 'my/${this.docs.service_name}':
source: service
service: ${this.docs.service_name}
source_path: ${this.docs.service_name}`;
this.config.app.full = `
mounts:
'my/${this.docs.service_name}':
source: service
service: ${this.docs.service_name}
source_path: ${this.docs.service_name}
`.trim();
this.config.app.commented = `
# ${this.name} allows you to define a file store that can be shared between different application containers.
# The service enables a new kind of 'mount' that refers to a shared service rather than to a local directory.
# After you have declared the service ${this.docs.service_name} in your 'services.yaml' file, add an entry
# to your mounts list for it. Upsun Fixed is read-only by default after the build process is completed, and defining mounts
# is the only way to set aside writable disk on a deployed application. Consult the mounts documentation
# (https://docs.upsun.com/anchors/fixed/app/reference/mounts/) for more details.
# Note that you do not need to add a relationship to point to the ${this.docs.service_name} service. That is handled automatically by the system.
mounts:
# Declare the writable mount path 'my/files' on the application container.
'my/${this.docs.service_name}':
# The source for the mount is defined as the service '${this.docs.service_name}' rather than a local directory.
source: service
service: ${this.docs.service_name}
# The source_path specifies the path within the network service that the mount points to. It is
# often easiest to have it match the name of the mount point itself but that is not required.
source_path: ${this.docs.service_name}
`.trim();
}
}
/**
* The Varnish class is a special case of a Service.
*
* It modifies the template strings generated for Varnish:
*
* - The 'relationships' block comments require additional information for Varnish.
* - The services.yaml file looks fundamentally different from other services, so the
* template strings are redefined completely here.
* - Varnish requires you to modify 'routes.yaml' to disable the router cache and
* direct requests to Varnish before the application itself, so those new template
* strings are defined and included in the new `files` property.
*
*/
class Varnish extends Service {
constructor(data) {
super(data);
this.config.app.commented = `
# The relationships block defines how services are mapped within your application.
relationships:
# At this time Upsun Fixed does not support circular relationships between services or
# applications, which means you cannot add a relationship in your .platform.app.yaml that
# points to the ${this.name} service. If you do so then one of the relationships will be
# skipped and the connection will not work. This limitation may be lifted in the future. See
# https://docs.upsun.com/anchors/fixed/services/${this.type}/#circular-relationships for
# more information.
# The ${this.name} service does offer an '${this.endpoint}' endpoint, which provides
# access and debugging tools that can be defined as a relationship.
${this.docs.relationship_name}: "${this.docs.service_name}:${this.endpoint}"
# You can then access the '${this.docs.relationship_name}' relationship over HTTP. Consult the ${this.name}
# documentation (https://docs.upsun.com/anchors/fixed/services/${this.type}/#stats-endpoint) to see
# the full list of paths provided by the '${this.endpoint}' endpoint.
# Note that because of the circular relationship issue noted above this cannot be done on the application
# that Varnish is forwarding to. It will need to be run on a separate application container.
`.trim();
this.config.services.snippet = `
${this.docs.service_name}:
type: ${this.type}:${this.recommended}
relationships:
application: 'app:http'
configuration:
vcl: !include
type: string
path: config.vcl
`.trim();
this.config.services.full = this.config.services.snippet;
this.config.services.commented = `
# The name given to the ${this.name} service (lowercase alphanumeric only).
${this.docs.service_name}:
# The type of your service (${this.type}), which uses the format
# 'type:version'. Be sure to consult the ${this.name} documentation
# (https://docs.upsun.com/anchors/fixed/services/${this.type}/#supported-versions)
# when choosing a version. If you specify a version number which is not available,
# the CLI will return an error.
type: ${this.type}:${this.recommended}
# The 'relationships' block defines a relationship ('application') to the application
# container ('app') using the 'http' endpoint, and is what allows ${this.name} to
# talk to the application container.
relationships:
application: 'app:http'
# The 'configuration' block is required, and references the VCL file (config.vcl) which
# is relative to the .platform directory.
configuration:
vcl: !include
type: string
path: config.vcl
`.trim();
this.config.routes.snippet = `
"https://{default}/":
type: upstream
upstream: "${this.type}:http"
cache:
enabled: false
`.trim();
this.config.routes.full = this.config.routes.snippet;
this.config.routes.commented = `
# Enable ${this.name}, by pointing to the '${this.docs.service_name}' service. This will map all
# incoming requests to the ${this.name} service rather than the application.
# ${this.name} will then, based on the VCL file defined in 'services.yaml',
# forward requests to the application as appropriate.
"https://{default}/":
type: upstream
upstream: "${this.type}:http"
# Disable the router cache as it is now entirely redundant with ${this.name}.
cache:
enabled: false
`.trim();
}
}
/**
* The VaultKms class is a special case of a service.
*
* It modifies the template strings generated for Vault KMS:
*
* - The services.yaml file requires special configuration
*
*/
class VaultKms extends Service {
constructor(data) {
super(data);
this.config.services.snippet = `
${this.docs.service_name}:
type: ${this.type}:${this.recommended}
disk: ${this.min_disk_size}
configuration:
endpoints:
manage_keys:
- policy: admin
key: vault-sign
type: sign
- policy: sign
key: vault-sign
type: sign
- policy: verify
key: vault-sign
type: sign
`.trim();
this.config.services.full = this.config.services.snippet;
this.config.services.commented = `
# The name given to the ${this.name} service (lowercase alphanumeric only).
${this.docs.service_name}:
# The type of your service (${this.type}), which uses the format
# 'type:version'. Be sure to consult the ${this.name} documentation
# (https://docs.upsun.com/anchors/fixed/services/${this.type}/#supported-versions)
# when choosing a version. If you specify a version number which is not available,
# the CLI will return an error.
type: ${this.type}:${this.recommended}
# The disk attribute is the size of the persistent disk (in MB) allocated to the service.
disk: ${this.min_disk_size}
# The 'configuration' block is required and defines the endpoints available for
# you to use in your app.
configuration:
endpoints:
manage_keys:
- policy: admin
key: vault-sign
type: sign
- policy: sign
key: vault-sign
type: sign
- policy: verify
key: vault-sign
type: sign
`.trim();
}
}
module.exports = {
Runtime,
Service,
NetworkStorage,
Varnish,
VaultKms
};