UNPKG

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
'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 };