├── .eslintrc ├── .github └── workflows │ ├── npm-publish.yaml │ └── quality-assurance.yaml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src └── platformsh.js └── test ├── testdata ├── ENV.json ├── ENV_runtime.json ├── PLATFORM_APPLICATION.json ├── PLATFORM_RELATIONSHIPS.json ├── PLATFORM_ROUTES.json └── PLATFORM_VARIABLES.json └── tests.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "env": { 4 | "es6": true, 5 | "node": true 6 | }, 7 | 8 | "globals": { 9 | "document": false, 10 | "escape": false, 11 | "navigator": false, 12 | "unescape": false, 13 | "window": false, 14 | "describe": true, 15 | "before": true, 16 | "it": true, 17 | "expect": true, 18 | "sinon": true 19 | }, 20 | 21 | "parserOptions": { 22 | "ecmaVersion": 6, 23 | "sourceType": "module", 24 | "ecmaFeatures": { 25 | "modules": true 26 | } 27 | }, 28 | 29 | "plugins": [ 30 | ], 31 | 32 | "rules": { 33 | "block-scoped-var": 2, 34 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 35 | "comma-dangle": [2, "never"], 36 | "comma-spacing": [2, { "before": false, "after": true }], 37 | "comma-style": [2, "last"], 38 | "complexity": 0, 39 | "consistent-this": 0, 40 | "curly": [2, "multi-line"], 41 | "default-case": 0, 42 | "dot-location": [2, "property"], 43 | "dot-notation": 0, 44 | "eol-last": 2, 45 | "eqeqeq": [2, "allow-null"], 46 | "func-names": 0, 47 | "func-style": 0, 48 | "generator-star-spacing": [2, "both"], 49 | "guard-for-in": 0, 50 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 51 | "indent": [2, 4, { "SwitchCase": 1 }], 52 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 53 | "linebreak-style": 0, 54 | "max-depth": 0, 55 | "max-len": [2, 160, 4], 56 | "max-nested-callbacks": 0, 57 | "max-params": 0, 58 | "max-statements": 0, 59 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 60 | "newline-after-var": [2, "always"], 61 | "new-parens": 2, 62 | "no-alert": 0, 63 | "no-array-constructor": 2, 64 | "no-bitwise": 0, 65 | "no-caller": 2, 66 | "no-catch-shadow": 0, 67 | "no-cond-assign": 2, 68 | "no-console": 0, 69 | "no-constant-condition": 0, 70 | "no-continue": 0, 71 | "no-control-regex": 2, 72 | "no-debugger": 2, 73 | "no-delete-var": 2, 74 | "no-div-regex": 0, 75 | "no-dupe-args": 2, 76 | "no-dupe-keys": 2, 77 | "no-duplicate-case": 2, 78 | "no-else-return": 2, 79 | "no-empty": 0, 80 | "no-empty-character-class": 2, 81 | "no-eq-null": 0, 82 | "no-eval": 2, 83 | "no-ex-assign": 2, 84 | "no-extend-native": 2, 85 | "no-extra-bind": 2, 86 | "no-extra-boolean-cast": 2, 87 | "no-extra-parens": 0, 88 | "no-extra-semi": 0, 89 | "no-extra-strict": 0, 90 | "no-fallthrough": 2, 91 | "no-floating-decimal": 2, 92 | "no-func-assign": 2, 93 | "no-implied-eval": 2, 94 | "no-inline-comments": 0, 95 | "no-inner-declarations": [2, "functions"], 96 | "no-invalid-regexp": 2, 97 | "no-irregular-whitespace": 2, 98 | "no-iterator": 2, 99 | "no-label-var": 2, 100 | "no-labels": 2, 101 | "no-lone-blocks": 0, 102 | "no-lonely-if": 0, 103 | "no-loop-func": 0, 104 | "no-mixed-requires": 0, 105 | "no-mixed-spaces-and-tabs": [2, false], 106 | "no-multi-spaces": 2, 107 | "no-multi-str": 2, 108 | "no-multiple-empty-lines": [2, { "max": 1 }], 109 | "no-native-reassign": 2, 110 | "no-negated-in-lhs": 2, 111 | "no-nested-ternary": 0, 112 | "no-new": 2, 113 | "no-new-func": 2, 114 | "no-new-object": 2, 115 | "no-new-require": 2, 116 | "no-new-wrappers": 2, 117 | "no-obj-calls": 2, 118 | "no-octal": 2, 119 | "no-octal-escape": 2, 120 | "no-path-concat": 0, 121 | "no-plusplus": 0, 122 | "no-process-env": 0, 123 | "no-process-exit": 0, 124 | "no-proto": 2, 125 | "no-redeclare": 2, 126 | "no-regex-spaces": 2, 127 | "no-reserved-keys": 0, 128 | "no-restricted-modules": 0, 129 | "no-return-assign": 2, 130 | "no-script-url": 0, 131 | "no-self-compare": 2, 132 | "no-sequences": 2, 133 | "no-shadow": 0, 134 | "no-shadow-restricted-names": 2, 135 | "no-spaced-func": 2, 136 | "no-sparse-arrays": 2, 137 | "no-sync": 0, 138 | "no-ternary": 0, 139 | "no-throw-literal": 2, 140 | "no-trailing-spaces": 2, 141 | "no-undef": 2, 142 | "no-undef-init": 2, 143 | "no-undefined": 0, 144 | "no-underscore-dangle": 0, 145 | "no-unneeded-ternary": 2, 146 | "no-unreachable": 2, 147 | "no-unused-expressions": 0, 148 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 149 | "no-use-before-define": 2, 150 | "no-var": 0, 151 | "no-void": 0, 152 | "no-warning-comments": 0, 153 | "no-with": 2, 154 | "one-var": 0, 155 | "operator-assignment": 0, 156 | "operator-linebreak": [2, "before"], 157 | "padded-blocks": 0, 158 | "quote-props": 0, 159 | "quotes": [2, "single", "avoid-escape"], 160 | "semi": [2, "always"], 161 | "semi-spacing": 0, 162 | "sort-vars": 0, 163 | "space-before-blocks": [2, "always"], 164 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 165 | "space-in-brackets": 0, 166 | "space-in-parens": [2, "never"], 167 | "space-infix-ops": 2, 168 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 169 | "spaced-comment": [2, "always"], 170 | "strict": 0, 171 | "use-isnan": 2, 172 | "valid-jsdoc": 0, 173 | "valid-typeof": 2, 174 | "vars-on-top": 2, 175 | "wrap-iife": [2, "any"], 176 | "wrap-regex": 0, 177 | "yoda": [2, "never"] 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish (npm) 3 | on: 4 | push: ~ 5 | 6 | jobs: 7 | deploy: 8 | name: 'Publish to npm' 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: '10.x' 15 | registry-url: 'https://registry.npmjs.org' 16 | - run: npm install 17 | - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | run: npm publish 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/quality-assurance.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Quality Assurance 3 | on: 4 | push: ~ 5 | pull_request: ~ 6 | 7 | jobs: 8 | build: 9 | name: '[Build/test] Node.js ${{ matrix.nodejs }}' 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | nodejs: [ '10', '12', '14' ] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.nodejs }} 19 | - run: npm install 20 | - run: npm run build --if-present 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Dependency directory 6 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 7 | node_modules 8 | 9 | .DS_Store 10 | 11 | lib 12 | 13 | package-lock.json 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | webpack.* 4 | .babelrc 5 | .eslintrc 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.4.1] - 2021-02-03 4 | 5 | ### Added 6 | 7 | * GitHub actions for tests (`quality-assurance.yaml`) and publishing to npm (`npm-publish.yaml`). 8 | 9 | ### Changed 10 | 11 | * `config` method can now get an object `{ varPrefix: string }` to specify a different environment variables prefix. 12 | 13 | ### Removed 14 | 15 | * CircleCI action config. 16 | 17 | ## [2.3.1] - 2019-11-04 18 | 19 | ### Added 20 | 21 | * `CHANGELOG` added. 22 | * `onDedicated` method that determines if the current environment is a Platform.sh Dedicated environment. Replaces deprecated `onEnterprise` method. 23 | 24 | ### Changed 25 | 26 | * Deprecates `onEnterprise` method - which is for now made to wrap around the added `onDedicated` method. `onEnterprise` **will be removed** in a future release, so update your projects to use `onDedicated` instead as soon as possible. 27 | 28 | ## [2.3.0] - 2019-09-19 29 | 30 | ### Added 31 | 32 | * `getPrimaryRoute` method for accessing routes marked "primary" in `routes.yaml`. 33 | * `getUpstreamRoutes` method returns an object map that includes only those routes that point to a valid upstream. 34 | 35 | ## [2.2.5] - 2019-06-04 36 | 37 | ### Added 38 | 39 | * Credential formatter `puppeteerFormatter` that returns Puppeteer connection string for using [Headless Chrome](https://docs.platform.sh/configuration/services/headless-chrome.html) on Platform.sh. 40 | 41 | ## [2.2.1] - 2019-04-30 42 | 43 | ### Removed 44 | 45 | * Removes the strict guard in place on the `variables` method. 46 | 47 | ## [2.2.0] - 2019-04-24 48 | 49 | ### Changed 50 | 51 | * Checks for valid environments were relaxed to unbreak use during local development. 52 | 53 | ## [2.1.0] - 2019-03-22 54 | 55 | ### Added 56 | 57 | * `hasRelationship` method to verify relationship has been defined before attempting to access credentials for it. 58 | 59 | ### Changed 60 | 61 | * BSD-2-Clause to MIT license. 62 | 63 | ## [2.0.3] - 2019-03-06 64 | 65 | ### Added 66 | 67 | * CircleCI deploy hook added to publish to npm. 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Platform.sh 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Platform.sh Config Reader (Node.js) 2 | 3 | ![Quality Assurance](https://github.com/platformsh/config-reader-nodejs/workflows/Quality%20Assurance/badge.svg) 4 | 5 | This library provides a streamlined and easy to use way to interact with a Platform.sh environment. It offers utility methods to access routes and relationships more cleanly than reading the raw environment variables yourself. 6 | 7 | This library requires Node.js 10 or later. 8 | 9 | ## Install 10 | 11 | ```bash 12 | npm install platformsh-config --save 13 | ``` 14 | 15 | ## Usage Example 16 | 17 | Example: 18 | 19 | ```js 20 | const mysql = require('mysql2/promise'); 21 | const config = require("platformsh-config").config(); 22 | 23 | if (!config.isValidPlatform()) { 24 | process.exit('Not in a Platform.sh Environment.'); 25 | } 26 | 27 | const credentials = config.credentials('database'); 28 | 29 | const connection = await mysql.createConnection({ 30 | host: credentials.host, 31 | port: credentials.port, 32 | user: credentials.username, 33 | password: credentials.password, 34 | database: credentials.path 35 | }); 36 | 37 | // Do stuff with connection. 38 | 39 | // Note the use of config.port. 40 | app.listen(config.port, function() { 41 | console.log(`Listening on port ${config.port}`) 42 | }); 43 | ``` 44 | 45 | ## API Reference 46 | 47 | ### Create a config object 48 | 49 | ```php 50 | const config = require("platformsh-config").config(); 51 | ``` 52 | 53 | `config` is now a `Config` object that provides access to the Platform.sh environment. 54 | 55 | The `isValidPlatform()` method returns `true` if the code is running in a context that has Platform.sh environment variables defined. If it returns `false` then most other functions will throw exceptions if used. 56 | 57 | ### Inspect the environment 58 | 59 | The following methods return `true` or `false` to help determine in what context the code is running: 60 | 61 | ```js 62 | config.inBuild(); 63 | 64 | config.inRuntime(); 65 | 66 | config.onDedicated(); 67 | 68 | config.onProduction(); 69 | ``` 70 | 71 | > **Note:** 72 | > 73 | > Platform.sh will no longer refer to its [99.99% uptime SLA product](https://platform.sh/solutions/) as "Enterprise", but rather as "Dedicated". Configuration Reader libraries have in turn been updated to include an `onDedicated` method to replace `onEnterprise`. For now `onEnterprise` remains available. It now calls the new method and no breaking changes have been introduced. 74 | > 75 | > It is recommended that you update your projects to use `onDedicated` as soon as possible, as `onEnterprise` will be removed in a future version of this library. 76 | 77 | ### Read environment variables 78 | 79 | The following magic properties return the corresponding environment variable value. See the [Platform.sh documentation](https://docs.platform.sh/development/variables.html) for a description of each. 80 | 81 | The following are available both in Build and at Runtime: 82 | 83 | ```js 84 | config.applicationName; 85 | 86 | config.appDir; 87 | 88 | config.project; 89 | 90 | config.treeId; 91 | 92 | config.projectEntropy; 93 | ``` 94 | 95 | The following are available only if `inRuntime()` returned `true`: 96 | 97 | ```js 98 | config.branch; 99 | 100 | config.documentRoot; 101 | 102 | config.smtpHost; 103 | 104 | config.environment; 105 | 106 | config.socket; 107 | 108 | config.port; 109 | ``` 110 | 111 | By default, Platform.sh environment variables are prefixed with `PLATFORM_`. In some cases, you might need to change this default in order to have access to environment variables at build time (like with [create-react-app](https://create-react-app.dev/docs/adding-custom-environment-variables/)). 112 | 113 | You can do this like so: 114 | ```js 115 | const config = require("platformsh-config").config({ varPrefix: "MY_PREFIX_" }); 116 | ``` 117 | 118 | ### Reading service credentials 119 | 120 | [Platform.sh services](https://docs.platform.sh/configuration/services.html) are defined in a `services.yaml` file, and exposed to an application by listing a `relationship` to that service in the application's `.platform.app.yaml` file. User, password, host, etc. information is then exposed to the running application in the `PLATFORM_RELATIONSHIPS` environment variable, which is a base64-encoded JSON string. The following method allows easier access to credential information than decoding the environment variable yourself. 121 | 122 | ```js 123 | creds = config.credentials('database'); 124 | ``` 125 | 126 | The return value of `credentials()` is a an object matching the relationship JSON object, which includes the appropriate user, password, host, database name, and other pertinent information. See the [Service documentation](https://docs.platform.sh/configuration/services.html) for your service for the exact structure and meaning of each property. In most cases that information can be passed directly to whatever other client library is being used to connect to the service. 127 | 128 | To make sure that a relationship is defined before you try to access credentials out of it, use the `hasRelationship()` method: 129 | 130 | ```js 131 | if (config.hasRelationship('database') { 132 | creds = config.credentials('database'); 133 | // ... 134 | } 135 | ``` 136 | 137 | ## Formatting service credentials 138 | 139 | In some cases the library being used to connect to a service wants its credentials formatted in a specific way; it could be a DSN string of some sort or it needs certain values concatenated to the database name, etc. For those cases you can use "Credential Formatters". A Credential Formatter is any function that takes a credentials object and returns any type, since the library may want different types. 140 | 141 | Credential Formatters can be registered on the configuration object, and a few are included out of the box. That allows 3rd party libraries to ship their own formatters that can be easily integrated into the `Config` object to allow easier use. 142 | 143 | ```js 144 | function formatMyService(credentials) { 145 | return "some string based on credentials"; 146 | } 147 | 148 | // Call this in setup. 149 | config.registerFormatter("my_service", formatMyService); 150 | 151 | 152 | // Then call this method to get the formatted version 153 | 154 | formatted = config.formattedCredentials("database", "my_service"); 155 | ``` 156 | 157 | The first parameter is the name of a relationship defined in `.platform.app.yaml`. The second is a formatter that was previously registered with `registerFormatter()`. If either the service or formatter is missing an exception will be thrown. The type of `formatted` will depend on the formatter function and can be safely passed directly to the client library. 158 | 159 | Two formatters are included out of the box: 160 | 161 | * `solr-node` returns an object appropriate for the `solr-node` library. `solr-node` needs the collection name on its own while the relationship's `path` property by default is a full URL path. This formatter handles that conversion. 162 | * `mongodb` returns a DSN to use with the `mongodb` client library's `connect()` method. Note that the credentials object is still needed to pass the database name (the `path property`) to the `db()` method. 163 | 164 | ### Reading Platform.sh variables 165 | 166 | Platform.sh allows you to define arbitrary variables that may be available at build time, runtime, or both. They are stored in the `PLATFORM_VARIABLES` environment variable, which is a base64-encoded JSON string. 167 | 168 | The following two methods allow access to those values from your code without having to bother decoding the values yourself: 169 | 170 | ```js 171 | config.variables(); 172 | ``` 173 | 174 | This method returns an associative array of all variables defined. Usually this method is not necessary and `config.variable()` is preferred. 175 | 176 | ```js 177 | config.variable("foo", "default"); 178 | ``` 179 | 180 | This method looks for the "foo" variable. If found, it is returned. If not, the optional second parameter is returned as a default. 181 | 182 | ### Reading Routes 183 | 184 | [Routes](https://docs.platform.sh/configuration/routes.html) on Platform.sh define how a project will handle incoming requests; that primarily means what application container will serve the request, but it also includes cache configuration, TLS settings, etc. Routes may also have an optional ID, which is the preferred way to access them. 185 | 186 | ```js 187 | config.getRoute("main"); 188 | ``` 189 | 190 | The `getRoute()` method takes a single string for the route ID ("main" in this case) and returns the corresponding route object. If the route is not found it will throw an exception. 191 | 192 | To access all routes, or to search for a route that has no ID, the `routes()` method returns a list of all route objects keyed by their URL. That mirrors the structure of the `PLATFORM_ROUTES` environment variable. 193 | 194 | If called in the build phase an exception is thrown. 195 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "platformsh-config", 3 | "version": "2.4.1", 4 | "description": "Helper for running nodejs applications on Platform.sh", 5 | "main": "lib/platformsh.js", 6 | "keywords": [ 7 | "platformsh", 8 | "paas" 9 | ], 10 | "author": "Larry Garfield (https://platform.sh)", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/platformsh/config-reader-nodejs/issues" 14 | }, 15 | "directories": { 16 | "lib": "lib", 17 | "src": "src", 18 | "test": "tests" 19 | }, 20 | "scripts": { 21 | "lint": "./node_modules/eslint/bin/eslint.js src/", 22 | "prepare": "npm run build", 23 | "build": "mkdir -p ./lib && cp src/platformsh.js lib/platformsh.js", 24 | "test": "NODE_ENV=test mocha --reporter spec" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/platformsh/config-reader-nodejs.git" 29 | }, 30 | "homepage": "https://github.com/platformsh/config-reader-nodejs#readme", 31 | "devDependencies": { 32 | "assert": "^1.4.1", 33 | "eslint": "^5.14.1", 34 | "mocha": "^6.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/platformsh.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class NotValidPlatformError extends Error {} 4 | 5 | class BuildTimeVariableAccessError extends Error {} 6 | 7 | class NoCredentialFormatterFoundError extends Error {} 8 | 9 | /** 10 | * Decodes a Platform.sh environment variable. 11 | * 12 | * @param {string} value 13 | * Base64-encoded JSON (the content of an environment variable). 14 | * 15 | * @return {any} 16 | * An associative array (if representing a JSON object), or a scalar type. 17 | */ 18 | function decode(value) { 19 | return JSON.parse(Buffer.from(value, 'base64')); 20 | } 21 | 22 | /** 23 | * Class representing a Platform.sh environment configuration. 24 | */ 25 | class Config { 26 | 27 | constructor(env = null, varPrefix = 'PLATFORM_') { 28 | this.environmentVariables = env || process.env; 29 | this.varPrefix = varPrefix; 30 | 31 | // Node doesn't support pre-defined object properties in classes, so 32 | // this is mostly for documentation but also to ensure there's always 33 | // a legal defined value. 34 | this.routesDef = []; 35 | this.relationshipsDef = []; 36 | this.variablesDef = []; 37 | this.applicationDef = []; 38 | this.credentialFormatters = {}; 39 | 40 | let routes = this._getValue('ROUTES'); 41 | if (routes) { 42 | this.routesDef = decode(routes); 43 | for (let [url, route] of Object.entries(this.routesDef)) { 44 | route['url'] = url; 45 | } 46 | } 47 | 48 | let relationships = this._getValue('RELATIONSHIPS'); 49 | if (relationships) { 50 | this.relationshipsDef = decode(relationships); 51 | } 52 | 53 | this.registerFormatter('solr-node', nodeSolrFormatter); 54 | this.registerFormatter('mongodb', mongodbFormatter); 55 | this.registerFormatter('puppeteer', puppeteerFormatter); 56 | 57 | let variables = this._getValue('VARIABLES'); 58 | if (variables) { 59 | this.variablesDef = decode(variables); 60 | } 61 | 62 | let application = this._getValue('APPLICATION'); 63 | if (application) { 64 | this.applicationDef = decode(application); 65 | } 66 | } 67 | 68 | /** 69 | * Callback for formatting a set of credentials for a particular library. 70 | * 71 | * @callback registerFormatterCallback 72 | * @param {object} credentials 73 | * A credential array, as returned by the credentials() method. 74 | * @return {mixed} 75 | * The formatted credentials. The format will vary depending on the 76 | * client library it is intended for, but usually either a string or an object. 77 | */ 78 | 79 | /** 80 | * Adds a credential formatter to the configuration. 81 | * 82 | * A credential formatter is responsible for formatting the credentials for a relationship 83 | * in a way expected by a particular client library. For instance, it can take the credentials 84 | * from Platform.sh for a PostgreSQL database and format them into a URL string expected by 85 | * a particular PostgreSQL client library. Use the formattedCredentials() method to get 86 | * the formatted version of a particular relationship. 87 | * 88 | * @param {string} name 89 | * The name of the formatter. This may be any arbitrary alphanumeric string. 90 | * @param {registerFormatterCallback} formatter 91 | * A callback function that will format relationship credentials for a specific client library. 92 | * @return {Config} 93 | * The called object, for chaining. 94 | */ 95 | registerFormatter(name, formatter) { 96 | this.credentialFormatters[name] = formatter; 97 | 98 | return this; 99 | } 100 | 101 | /** 102 | * Returns credentials for the specified relationship as formatted by the specified formatter. 103 | * 104 | * @param {string} relationship 105 | * The relationship whose credentials should be formatted. 106 | * @param {string} formatter 107 | * The registered formatter to use. This must match a formatter previously registered 108 | * with registerFormatter(). 109 | * @return mixed 110 | * The credentials formatted with the given formatter. 111 | */ 112 | formattedCredentials(relationship, formatter) { 113 | if (!this.credentialFormatters.hasOwnProperty(formatter)) { 114 | throw new NoCredentialFormatterFoundError(`There is no credential formatter named "${formatter}" registered. Did you remember to call registerFormatter()?`); 115 | } 116 | 117 | return this.credentialFormatters[formatter](this.credentials(relationship)); 118 | } 119 | 120 | /** 121 | * Checks whether the code is running on a platform with valid environment variables. 122 | * 123 | * @return {boolean} 124 | * True if configuration can be used, false otherwise. 125 | */ 126 | isValidPlatform() { 127 | return Boolean(this._getValue('APPLICATION_NAME')); 128 | } 129 | 130 | /** 131 | * Checks whether the code is running in a build environment. 132 | * 133 | * If false, it's running at deploy time. 134 | * 135 | * @return {boolean} 136 | */ 137 | inBuild() { 138 | return this.isValidPlatform() && !this._getValue('ENVIRONMENT'); 139 | } 140 | 141 | /** 142 | * Checks whether the code is running in a runtime environment. 143 | * 144 | * @return {boolean} 145 | */ 146 | inRuntime() { 147 | return this.isValidPlatform() && Boolean(this._getValue('ENVIRONMENT')); 148 | } 149 | 150 | /** 151 | * Determines if the current environment is a Platform.sh Dedicated environment. 152 | * 153 | * @deprecated 154 | * 155 | * The Platform.sh "Enterprise" will soon be referred to exclusively as 156 | * "Dedicated". the `onEnterprise` method remains available for now, but it 157 | * will be removed in a future version of this library. 158 | * 159 | * It is recommended that you update your projects to use `onDedicated` as 160 | * soon as possible. 161 | * 162 | * @return {boolean} 163 | * True on an Dedicated environment, False otherwise. 164 | */ 165 | onEnterprise() { 166 | return this.onDedicated(); 167 | } 168 | 169 | /** 170 | * Determines if the current environment is a Platform.sh Dedicated environment. 171 | * 172 | * @return {boolean} 173 | * True on an Dedicated environment, False otherwise. 174 | */ 175 | onDedicated() { 176 | return this.isValidPlatform() && this._getValue('MODE') === 'enterprise'; 177 | } 178 | 179 | /** 180 | * Determines if the current environment is a production environment. 181 | * 182 | * Note: There may be a few edge cases where this is not entirely correct on Enterprise, 183 | * if the production branch is not named `production`. In that case you'll need to use 184 | * your own logic. 185 | * 186 | * @return {boolean} 187 | * True if the environment is a production environment, false otherwise. 188 | * It will also return false if not running on Platform.sh or in the build phase. 189 | */ 190 | onProduction() { 191 | if (!this.inRuntime()) { 192 | return; 193 | } 194 | 195 | let prodBranch = this.onEnterprise() ? 'production' : 'master'; 196 | 197 | return this._getValue('BRANCH') === prodBranch; 198 | } 199 | 200 | /** 201 | * Returns the routes definition. 202 | * 203 | * @return {object} 204 | * The routes definition object. 205 | * @throws {Error} 206 | * If the routes are not accessible due to being in the wrong environment. 207 | */ 208 | routes() { 209 | if (this.inBuild()) { 210 | throw new BuildTimeVariableAccessError('Routes are not available during the build phase.'); 211 | } 212 | 213 | if (!this.routesDef) { 214 | throw new NotValidPlatformError('No routes defined. Are you sure you are running on Platform.sh?'); 215 | } 216 | 217 | return this.routesDef; 218 | } 219 | 220 | /** 221 | * Returns the primary route. 222 | * 223 | * The primary route is the one marked primary in `routes.yaml`, or else 224 | * the first non-redirect route in that file if none are marked. 225 | * 226 | * @return {object} 227 | * The route definition. The generated URL of the route is added as a "url" key. 228 | */ 229 | getPrimaryRoute() { 230 | // eslint-disable-next-line no-unused-vars 231 | for (const [url, route] of Object.entries(this.routes())) { 232 | if (route.primary === true) { 233 | return route; 234 | } 235 | } 236 | 237 | throw new Error(`No primary route found. This isn't supposed to happen.`); 238 | } 239 | 240 | /** 241 | * Returns just those routes that point to a valid upstream. 242 | * 243 | * This method is similar to routes(), but filters out redirect routes that are rarely 244 | * useful for app configuration. If desired it can also filter to just those routes 245 | * whose upstream is a given application name. To retrieve routes that point to the 246 | * current application where the code is being run, use: 247 | * 248 | * routes = config.getUpstreamRoutes(config.applicationName); 249 | * 250 | * @param {string|null} appName 251 | * The name of the upstream app on which to filter, if any. 252 | * @return {object} 253 | * An object map of route definitions. 254 | */ 255 | getUpstreamRoutes(appName = null) { 256 | // Because routes is an object/dictionary, we can't just filter it directly. 257 | // Verbose way it is. 258 | 259 | const routes = this.routes(); 260 | const filter = route => { 261 | return route.type === 'upstream' 262 | // On Dedicated, the upstream name sometimes is `app:http` instead of just `app`. 263 | // If no name is specified then don't bother checking. 264 | && (!appName || appName === route.upstream.split(':')[0]); 265 | }; 266 | 267 | let ret = {}; 268 | 269 | Object.keys(routes).forEach(function(key) { 270 | if (filter(routes[key])) { 271 | ret[key] = routes[key]; 272 | } 273 | }); 274 | 275 | return ret; 276 | } 277 | 278 | /** 279 | * Returns a single route definition. 280 | * 281 | * Note: If no route ID was specified in routes.yaml then it will not be possible 282 | * to look up a route by ID. 283 | * 284 | * @param {string} id 285 | * The ID of the route to load. 286 | * @return {object} 287 | * The route definition. The generated URL of the route is added as a "url" key. 288 | * @throws {Error} 289 | * If there is no route by that ID, an exception is thrown. 290 | */ 291 | getRoute(id) { 292 | // eslint-disable-next-line no-unused-vars 293 | for (const [url, route] of Object.entries(this.routes())) { 294 | if (route.id === id) { 295 | return route; 296 | } 297 | } 298 | 299 | throw new Error(`No such route id found: ${id}`); 300 | } 301 | 302 | /** 303 | * Determines if a relationship is defined, and thus has credentials available. 304 | * 305 | * @param {string} relationship 306 | * The name of the relationship to check. 307 | * @return {boolean} 308 | * True if the relationship is defined, false otherwise. 309 | */ 310 | hasRelationship(relationship) { 311 | return Boolean(this.relationshipsDef[relationship]); 312 | } 313 | 314 | /** 315 | * Retrieves the credentials for accessing a relationship. 316 | * 317 | * The relationship must be defined in the .platform.app.yaml file. 318 | * 319 | * @param {string} relationship 320 | * The relationship name as defined in .platform.app.yaml. 321 | * @param {int} index 322 | * The index within the relationship to access. This is always 0, but reserved 323 | * for future extension. 324 | * @return {object} 325 | * The credentials array for the service pointed to by the relationship. 326 | * @throws {Error} 327 | * Thrown if called in a context that has no relationships (eg, in build) 328 | * @throws {RangeError} 329 | * If the relationship/index pair requested does not exist. 330 | */ 331 | credentials(relationship, index = 0) { 332 | if (!this.relationshipsDef) { 333 | if (this.inBuild()) { 334 | throw new BuildTimeVariableAccessError('Relationships are not available during the build phase.'); 335 | } 336 | throw new NotValidPlatformError('No relationships are defined. Are you sure you are on Platform.sh?' 337 | + ' If you\'re running on your local system you may need to create a tunnel' 338 | + ' to access your environment services. See https://docs.platform.sh/gettingstarted/local/tethered.html'); 339 | } 340 | 341 | if (!this.relationshipsDef[relationship]) { 342 | throw new RangeError(`No relationship defined: ${relationship}. Check your .platform.app.yaml file.`); 343 | } 344 | if (!this.relationshipsDef[relationship][index]) { 345 | throw new RangeError(`No index ${index} defined for relationship: ${relationship}. Check your .platform.app.yaml file.`); 346 | } 347 | 348 | return this.relationshipsDef[relationship][index]; 349 | } 350 | 351 | /** 352 | * Returns a variable from the VARIABLES array. 353 | * 354 | * Note: variables prefixed with `env:` can be accessed as normal environment variables. 355 | * This method will return such a variable by the name with the prefix still included. 356 | * Generally it's better to access those variables directly. 357 | * 358 | * @param {string} name 359 | * The name of the variable to retrieve. 360 | * @param {any} defaultValue 361 | * The default value to return if the variable is not defined. Defaults to null. 362 | * @return mixed 363 | * The value of the variable, or the specified default. This may be a string or an array. 364 | */ 365 | variable(name, defaultValue = null) { 366 | return this.variablesDef.hasOwnProperty(name) ? this.variablesDef[name] : defaultValue; 367 | } 368 | 369 | /** 370 | * Returns the full variables array. 371 | * 372 | * If you're looking for a specific variable, the variable() method is a more robust option. 373 | * This method is for cases where you want to scan the whole variables list looking for a pattern. 374 | * 375 | * @return {object} 376 | * The full variables definition. 377 | */ 378 | variables() { 379 | 380 | return this.variablesDef; 381 | } 382 | 383 | /** 384 | * Returns the application definition object. 385 | * 386 | * This is, approximately, the .platform.app.yaml file as a nested array. However, it also 387 | * has other information added by Platform.sh as part of the build and deploy process. 388 | * 389 | * @return {object} 390 | * The application definition object. 391 | */ 392 | application() { 393 | if (!this.applicationDef) { 394 | throw new NotValidPlatformError('No application definition is available. Are you sure you are running on Platform.sh?'); 395 | } 396 | 397 | return this.applicationDef; 398 | } 399 | 400 | /** 401 | * The absolute path to the application directory. 402 | * 403 | * @returns {string} 404 | */ 405 | get appDir() { 406 | return this._buildValue('APP_DIR', 'appDir'); 407 | } 408 | 409 | /** 410 | * The name of the application container, as configured in the .platform.app.yaml file. 411 | * 412 | * @returns {string} 413 | */ 414 | get applicationName() { 415 | return this._buildValue('APPLICATION_NAME', 'applicationName'); 416 | } 417 | 418 | /** 419 | * The project ID. 420 | * 421 | * @returns {string} 422 | */ 423 | get project() { 424 | return this._buildValue('PROJECT', 'project'); 425 | } 426 | 427 | /** 428 | * The ID of the tree the application was built from. 429 | * 430 | * This is essentially the SHA hash of the tree in Git. If you need a unique ID 431 | * for each build for whatever reason this is the value you should use. 432 | * 433 | * @returns {string} 434 | */ 435 | get treeId() { 436 | return this._buildValue('TREE_ID', 'treeId'); 437 | } 438 | 439 | /** 440 | * The project project entropy value. 441 | * 442 | * This random value is created when the project is first created, which is then stable 443 | * throughout the project’s life. It should be used for application-specific unique-instance 444 | * hashing. 445 | * 446 | * @returns {string} 447 | */ 448 | get projectEntropy() { 449 | return this._buildValue('PROJECT_ENTROPY', 'projectEntropy'); 450 | } 451 | 452 | /** 453 | * The name of the Git branch. 454 | * 455 | * @returns {string} 456 | */ 457 | get branch() { 458 | return this._runtimeValue('BRANCH', 'branch'); 459 | } 460 | 461 | /** 462 | * The name of the environment generated by the name of the Git branch. 463 | * 464 | * @returns {string} 465 | */ 466 | get environment() { 467 | return this._runtimeValue('ENVIRONMENT', 'environment'); 468 | } 469 | 470 | /** 471 | * The absolute path to the web document root, if applicable. 472 | * 473 | * @returns {string} 474 | */ 475 | get documentRoot() { 476 | return this._runtimeValue('DOCUMENT_ROOT', 'documentRoot'); 477 | } 478 | 479 | /** 480 | * The SMTP host to use for sending email. 481 | * 482 | * If empty, it means email sending is disabled in this environment. 483 | * 484 | * @returns {string} 485 | */ 486 | get smtpHost() { 487 | return this._runtimeValue('SMTP_HOST', 'smtpHost'); 488 | } 489 | 490 | /** 491 | * The TCP port number the application should listen to for incoming requests. 492 | * 493 | * @returns {string} 494 | */ 495 | get port() { 496 | if (this.inBuild()) { 497 | throw new BuildTimeVariableAccessError(`The "port" variable is not available during build time.`); 498 | } 499 | let value = this.environmentVariables['PORT']; 500 | if (!value) { 501 | throw new NotValidPlatformError(`The "port" variable is not defined. Are you sure you're running on Platform.sh?`); 502 | } 503 | return value; 504 | } 505 | 506 | /** 507 | * The Unix socket the application should listen to for incoming requests. 508 | * 509 | * @returns {string} 510 | */ 511 | get socket() { 512 | if (this.inBuild()) { 513 | throw new BuildTimeVariableAccessError(`The "socket" variable is not available during build time.`); 514 | } 515 | let value = this.environmentVariables['SOCKET']; 516 | if (!value) { 517 | throw new NotValidPlatformError(`The "socket" variable is not defined. Are you sure you're running on Platform.sh?`); 518 | } 519 | return value; 520 | } 521 | 522 | /** 523 | * Returns a build-safe variable's value if defined, or throws an error. 524 | * 525 | * @param {string} property 526 | * The name of the environment variable, without prefix. 527 | * @param {string} humanName 528 | * The human-readable name of the property to be used in error messages. 529 | * @return {string} 530 | * The variable's value. 531 | * @private 532 | */ 533 | _buildValue(property, humanName) { 534 | let value = this._getValue(property); 535 | if (!value) { 536 | throw new NotValidPlatformError(`The "${humanName}" variable is not defined. Are you sure you're running on Platform.sh?`); 537 | } 538 | return value; 539 | } 540 | 541 | /** 542 | * Returns a runtime-only variable's value if defined, or throws an error. 543 | * 544 | * @param {string} property 545 | * The name of the environment variable, without prefix. 546 | * @param {string} humanName 547 | * The human-readable name of the property to be used in error messages. 548 | * @return {string} 549 | * The variable's value. 550 | * @private 551 | */ _runtimeValue(property, humanName) { 552 | if (this.inBuild()) { 553 | throw new BuildTimeVariableAccessError(`The "${humanName}" variable is not available during build time.`); 554 | } 555 | let value = this._getValue(property); 556 | if (!value) { 557 | throw new NotValidPlatformError(`The "${humanName}" variable is not defined. Are you sure you're running on Platform.sh?`); 558 | } 559 | return value; 560 | } 561 | 562 | /** 563 | * Reads an environment variable, taking the prefix into account. 564 | * 565 | * @param {string} name 566 | * The variable to read. 567 | * @return {string|null} 568 | */ 569 | _getValue(name) { 570 | let checkName = this.varPrefix + name.toUpperCase(); 571 | 572 | return this.environmentVariables[checkName] || null; 573 | } 574 | } 575 | 576 | 577 | /** 578 | * Returns a connection object appropriate for the solr-node library. 579 | * 580 | * @param credentials 581 | * A solr credentials object. 582 | * @returns {object} 583 | * A credentials object to pass to new SolrNode(). 584 | */ 585 | function nodeSolrFormatter(credentials) { 586 | return { 587 | host: credentials.host, 588 | port: credentials.port, 589 | core: credentials.path.split('/').slice(-1)[0], 590 | protocol: 'http' 591 | } 592 | } 593 | 594 | /** 595 | * Returns a connection string appropriate for the mongodb library. 596 | * 597 | * @param credentials 598 | * A mongodb credentials object 599 | * @returns {string} 600 | * A connection string to pass to MongoClient.connect(). 601 | */ 602 | function mongodbFormatter(credentials) { 603 | return `mongodb://${credentials["username"]}:${credentials["password"]}@${credentials["host"]}:${credentials["port"]}/${credentials["path"]}`; 604 | } 605 | 606 | /** 607 | * Returns a connection string appropriate for Puppeteer and headless Chrome. 608 | * @param cretentials 609 | * A chrome-headless credentials object 610 | * @returns {string} 611 | * A connection string to pass to puppeteer.connect(). 612 | */ 613 | function puppeteerFormatter(credentials) { 614 | return `http://${credentials["ip"]}:${credentials["port"]}`; 615 | } 616 | 617 | /** 618 | * Creates a new Config instance that represents the current environment. 619 | * 620 | * @returns {Config} 621 | */ 622 | function config({ varPrefix } = {}) { 623 | 624 | return new Config(null, varPrefix); 625 | } 626 | 627 | module.exports = { 628 | config 629 | }; 630 | 631 | // In testing, also expsoe the class so we can pass in test data. 632 | if (process.env.NODE_ENV === 'test') { 633 | module.exports.Config = Config; 634 | } 635 | -------------------------------------------------------------------------------- /test/testdata/ENV.json: -------------------------------------------------------------------------------- 1 | { 2 | "PLATFORM_APP_DIR": "/app", 3 | "PLATFORM_APPLICATION_NAME": "app", 4 | "PLATFORM_PROJECT": "test-project", 5 | "PLATFORM_TREE_ID": "abc123", 6 | "PLATFORM_PROJECT_ENTROPY": "def789", 7 | 8 | "SOME_VARIABLE": "some value", 9 | 10 | "CUSTOM_PREFIX_VARIABLE_WITH_CUSTOM_PREFIX": "with custom prefix" 11 | } 12 | -------------------------------------------------------------------------------- /test/testdata/ENV_runtime.json: -------------------------------------------------------------------------------- 1 | { 2 | "PLATFORM_BRANCH": "feature-x", 3 | "PLATFORM_ENVIRONMENT": "feature-x-hgi456", 4 | "PLATFORM_DOCUMENT_ROOT": "/app/web", 5 | "PLATFORM_SMTP_HOST": "1.2.3.4", 6 | "PORT": "8080", 7 | "SOCKET": "unix://tmp/blah.sock" 8 | } 9 | -------------------------------------------------------------------------------- /test/testdata/PLATFORM_APPLICATION.json: -------------------------------------------------------------------------------- 1 | { 2 | "disk" : 128, 3 | "size" : "AUTO", 4 | "timezone" : null, 5 | "mounts" : {}, 6 | "name" : "app", 7 | "hooks" : { 8 | "build" : "set -e\n", 9 | "deploy" : "set -e\n", 10 | "post_deploy" : null 11 | }, 12 | "runtime" : { 13 | "extensions" : [ 14 | "redis", 15 | "pdo_pgsql", 16 | "mongodb", 17 | "memcached" 18 | ] 19 | }, 20 | "variables" : {}, 21 | "type" : "php:7.2", 22 | "access" : { 23 | "ssh" : "contributor" 24 | }, 25 | "relationships" : { 26 | "database" : "mysql:mysql", 27 | "elasticsearch" : "elasticsearch:elasticsearch" 28 | }, 29 | "preflight" : { 30 | "ignored_rules" : [], 31 | "enabled" : true 32 | }, 33 | "web" : { 34 | "locations" : { 35 | "/" : { 36 | "headers" : {}, 37 | "passthru" : "/index.php", 38 | "allow" : true, 39 | "rules" : {}, 40 | "scripts" : true, 41 | "expires" : "-1s", 42 | "root" : "web" 43 | } 44 | }, 45 | "move_to_root" : false 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/testdata/PLATFORM_RELATIONSHIPS.json: -------------------------------------------------------------------------------- 1 | { 2 | "database" : [ 3 | { 4 | "scheme" : "mysql", 5 | "cluster" : "dtsla3sy7euhc-master-7rqtwti", 6 | "service" : "mysql", 7 | "username" : "user", 8 | "password" : "", 9 | "host" : "database.internal", 10 | "path" : "main", 11 | "public" : false, 12 | "fragment" : null, 13 | "ip" : "169.254.81.252", 14 | "query" : { 15 | "is_master" : true 16 | }, 17 | "rel" : "mysql", 18 | "type" : "mysql:10.2", 19 | "port" : 3306, 20 | "hostname" : "ihq65cmi2m7nd3svqpcrbjchyy.mysql.service._.us-2.platformsh.site" 21 | } 22 | ], 23 | "elasticsearch" : [ 24 | { 25 | "hostname" : "bwhgfsnjp7kzqlf7pd35dfg6mm.elasticsearch.service._.us-2.platformsh.site", 26 | "port" : 9200, 27 | "type" : "elasticsearch:5.4", 28 | "rel" : "elasticsearch", 29 | "query" : {}, 30 | "ip" : "169.254.250.214", 31 | "path" : null, 32 | "public" : false, 33 | "fragment" : null, 34 | "password" : null, 35 | "host" : "elasticsearch.internal", 36 | "username" : null, 37 | "service" : "elasticsearch", 38 | "cluster" : "dtsla3sy7euhc-master-7rqtwti", 39 | "scheme" : "http" 40 | } 41 | ], 42 | "solr" : [ 43 | { 44 | "ip" : "169.254.177.78", 45 | "host" : "solr.internal", 46 | "path" : "solr/collection1", 47 | "port" : 8080, 48 | "cluster" : "rjify4yjcwxaa-pr-6-3qodc7y", 49 | "service" : "solr", 50 | "type" : "solr:7.6", 51 | "rel" : "solr", 52 | "hostname" : "dg3c6xro44gl5tuxqbqf5flvhu.solr.service._.eu-3.platformsh.site", 53 | "scheme" : "solr" 54 | } 55 | ], 56 | "headless": [ 57 | { 58 | "service": "headless", 59 | "ip": "169.254.16.215", 60 | "hostname": "vjteswvo72bpyb33rgijkpooja.headless.service._.eu-3.platformsh.site", 61 | "cluster": "moqwtrvgc63mo-pr-5-kehpj4q", 62 | "host": "headless.internal", 63 | "rel": "http", 64 | "scheme": "http", 65 | "type": "chrome-headless:73", 66 | "port": 9222 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /test/testdata/PLATFORM_ROUTES.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://www.master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/" : { 3 | "original_url" : "https://www.{default}/", 4 | "attributes" : {}, 5 | "type" : "upstream", 6 | "restrict_robots" : false, 7 | "tls" : { 8 | "client_authentication" : null, 9 | "min_version" : 771, 10 | "client_certificate_authorities" : [], 11 | "strict_transport_security" : { 12 | "include_subdomains" : null, 13 | "enabled" : true, 14 | "preload" : null 15 | } 16 | }, 17 | "upstream" : "app", 18 | "cache" : { 19 | "enabled" : true, 20 | "headers" : [ 21 | "Accept", 22 | "Accept-Language" 23 | ], 24 | "cookies" : [ 25 | "/^SS?ESS.*/" 26 | ], 27 | "default_ttl" : 0 28 | }, 29 | "http_access" : { 30 | "addresses" : [], 31 | "basic_auth" : {} 32 | }, 33 | "primary" : true, 34 | "id" : "main", 35 | "ssi" : { 36 | "enabled" : false 37 | } 38 | }, 39 | "https://www2.master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/" : { 40 | "original_url" : "https://www.{default}/", 41 | "attributes" : {}, 42 | "type" : "upstream", 43 | "restrict_robots" : false, 44 | "tls" : { 45 | "client_authentication" : null, 46 | "min_version" : 771, 47 | "client_certificate_authorities" : [], 48 | "strict_transport_security" : { 49 | "include_subdomains" : null, 50 | "enabled" : true, 51 | "preload" : null 52 | } 53 | }, 54 | "upstream" : "app", 55 | "cache" : { 56 | "enabled" : true, 57 | "headers" : [ 58 | "Accept", 59 | "Accept-Language" 60 | ], 61 | "cookies" : [ 62 | "/^SS?ESS.*/" 63 | ], 64 | "default_ttl" : 0 65 | }, 66 | "http_access" : { 67 | "addresses" : [], 68 | "basic_auth" : {} 69 | }, 70 | "primary" : false, 71 | "id" : "main2", 72 | "ssi" : { 73 | "enabled" : false 74 | } 75 | }, 76 | "https://www3.master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/" : { 77 | "original_url" : "https://www.{default}/", 78 | "attributes" : {}, 79 | "type" : "upstream", 80 | "restrict_robots" : false, 81 | "tls" : { 82 | "client_authentication" : null, 83 | "min_version" : 771, 84 | "client_certificate_authorities" : [], 85 | "strict_transport_security" : { 86 | "include_subdomains" : null, 87 | "enabled" : true, 88 | "preload" : null 89 | } 90 | }, 91 | "upstream" : "app2", 92 | "cache" : { 93 | "enabled" : true, 94 | "headers" : [ 95 | "Accept", 96 | "Accept-Language" 97 | ], 98 | "cookies" : [ 99 | "/^SS?ESS.*/" 100 | ], 101 | "default_ttl" : 0 102 | }, 103 | "http_access" : { 104 | "addresses" : [], 105 | "basic_auth" : {} 106 | }, 107 | "primary" : false, 108 | "id" : "main3", 109 | "ssi" : { 110 | "enabled" : false 111 | } 112 | }, 113 | "http://www.master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/" : { 114 | "id" : null, 115 | "to" : "https://www.master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/", 116 | "primary" : false, 117 | "original_url" : "http://www.{default}/", 118 | "http_access" : { 119 | "basic_auth" : {}, 120 | "addresses" : [] 121 | }, 122 | "restrict_robots" : false, 123 | "type" : "redirect" 124 | }, 125 | "https://master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/" : { 126 | "id" : null, 127 | "to" : "https://www.master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/", 128 | "primary" : false, 129 | "http_access" : { 130 | "addresses" : [], 131 | "basic_auth" : {} 132 | }, 133 | "tls" : { 134 | "client_authentication" : null, 135 | "min_version" : null, 136 | "strict_transport_security" : { 137 | "include_subdomains" : null, 138 | "enabled" : null, 139 | "preload" : null 140 | }, 141 | "client_certificate_authorities" : [] 142 | }, 143 | "restrict_robots" : false, 144 | "type" : "redirect", 145 | "attributes" : {}, 146 | "original_url" : "https://{default}/" 147 | }, 148 | "http://master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/" : { 149 | "to" : "https://master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/", 150 | "id" : null, 151 | "primary" : false, 152 | "http_access" : { 153 | "addresses" : [], 154 | "basic_auth" : {} 155 | }, 156 | "original_url" : "http://{default}/", 157 | "type" : "redirect", 158 | "restrict_robots" : false 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /test/testdata/PLATFORM_VARIABLES.json: -------------------------------------------------------------------------------- 1 | { 2 | "somevar": "someval" 3 | } 4 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert').strict; 4 | const psh = require('../src/platformsh.js'); 5 | const fs = require('fs'); 6 | 7 | function encode(value) { 8 | return Buffer.from(JSON.stringify(value)).toString('base64'); 9 | } 10 | 11 | function loadJsonFile(name) { 12 | return JSON.parse(fs.readFileSync(`test/testdata/${name}.json`, 'utf8')); 13 | } 14 | 15 | function deepClone(obj) { 16 | return JSON.parse(JSON.stringify(obj)); 17 | } 18 | 19 | describe("Config tests", () => { 20 | 21 | let mockEnvironmentBuild = []; 22 | let mockEnvironmentRuntime = []; 23 | 24 | before(() => { 25 | const env = loadJsonFile('ENV'); 26 | 27 | ['PLATFORM_APPLICATION', 'PLATFORM_VARIABLES'].forEach((item) => { 28 | env[item] = encode(loadJsonFile(item)); 29 | }); 30 | 31 | mockEnvironmentBuild = deepClone(env); 32 | 33 | ['PLATFORM_ROUTES', 'PLATFORM_RELATIONSHIPS'].forEach((item) => { 34 | env[item] = encode(loadJsonFile(item)); 35 | }); 36 | 37 | const envRuntime = loadJsonFile('ENV_runtime'); 38 | 39 | mockEnvironmentRuntime = {...env, ...envRuntime}; 40 | }); 41 | 42 | describe("isValidPlatform() tests", () => { 43 | 44 | it('Returns false when not on Platform.sh', () => { 45 | const c = new psh.Config(); 46 | 47 | assert.ok(!c.isValidPlatform()); 48 | }); 49 | 50 | it('Returns true when on Platform.sh, build time', () => { 51 | const c = new psh.Config(mockEnvironmentBuild); 52 | 53 | assert.ok(c.isValidPlatform()); 54 | }); 55 | 56 | it('Returns true when on Platform.sh, runtime', () => { 57 | const c = new psh.Config(mockEnvironmentRuntime); 58 | 59 | assert.ok(c.isValidPlatform()); 60 | }); 61 | }); 62 | 63 | describe("inBuid() tests", () => { 64 | 65 | it('Returns true in build environment', () => { 66 | const c = new psh.Config(mockEnvironmentBuild); 67 | 68 | assert.ok(c.inBuild()) 69 | }); 70 | 71 | it('Returns false in runtime environment', () => { 72 | const c = new psh.Config(mockEnvironmentRuntime); 73 | 74 | assert.ok(!c.inBuild()) 75 | }); 76 | }); 77 | 78 | 79 | describe("inRuntime() tests", () => { 80 | 81 | it('Returns true in runtime environment', () => { 82 | const c = new psh.Config(mockEnvironmentRuntime); 83 | 84 | assert.ok(c.inRuntime()); 85 | }); 86 | 87 | it('Returns false in build environment', () => { 88 | const c = new psh.Config(mockEnvironmentBuild); 89 | 90 | assert.ok(!c.inRuntime()); 91 | }); 92 | }); 93 | 94 | describe("onDedicated() tests", () => { 95 | 96 | it('Returns true in Dedicated environment', () => { 97 | const mockEnvironmentDedicated = deepClone(mockEnvironmentRuntime); 98 | mockEnvironmentDedicated['PLATFORM_MODE'] = 'enterprise'; 99 | 100 | const c = new psh.Config(mockEnvironmentDedicated); 101 | 102 | assert.ok(c.onDedicated()); 103 | }); 104 | 105 | it('Returns false in standard environment', () => { 106 | const c = new psh.Config(mockEnvironmentRuntime); 107 | 108 | assert.ok(!c.onDedicated()); 109 | }); 110 | }); 111 | 112 | describe("onProduction() tests", () => { 113 | 114 | it('Returns true on Dedicated production', () => { 115 | const mockEnvironmentDedicated = deepClone(mockEnvironmentRuntime); 116 | mockEnvironmentDedicated['PLATFORM_MODE'] = 'enterprise'; 117 | mockEnvironmentDedicated['PLATFORM_BRANCH'] = 'production'; 118 | 119 | const c = new psh.Config(mockEnvironmentDedicated); 120 | 121 | assert.ok(c.onProduction()); 122 | }); 123 | 124 | it('Returns false on Dedicated staging', () => { 125 | const mockEnvironmentDedicated = deepClone(mockEnvironmentRuntime); 126 | mockEnvironmentDedicated['PLATFORM_MODE'] = 'enterprise'; 127 | 128 | const c = new psh.Config(mockEnvironmentDedicated); 129 | 130 | assert.ok(!c.onProduction()); 131 | }); 132 | 133 | it('Returns true on standard master', () => { 134 | const mockEnvironmentProduction = deepClone(mockEnvironmentRuntime); 135 | mockEnvironmentProduction['PLATFORM_BRANCH'] = 'master'; 136 | 137 | const c = new psh.Config(mockEnvironmentProduction); 138 | 139 | assert.ok(c.onProduction()); 140 | }); 141 | 142 | it('Returns false on standard dev', () => { 143 | const c = new psh.Config(mockEnvironmentRuntime); 144 | 145 | assert.ok(!c.onProduction()); 146 | }); 147 | }); 148 | 149 | describe("Route tests", () => { 150 | 151 | it('loads all routes in runtime', () => { 152 | const c = new psh.Config(mockEnvironmentRuntime); 153 | 154 | const routes = c.routes(); 155 | 156 | assert.ok(typeof routes == 'object'); 157 | assert.equal(Object.keys(routes).length, 6); 158 | }); 159 | 160 | it('throws when loading routes in build time', () => { 161 | const c = new psh.Config(mockEnvironmentBuild); 162 | 163 | assert.throws(() => { 164 | const routes = c.routes(); 165 | }); 166 | }); 167 | 168 | it('gets the primary route', () => { 169 | const c = new psh.Config(mockEnvironmentRuntime); 170 | 171 | const route = c.getPrimaryRoute(); 172 | 173 | assert.equal(route['original_url'], 'https://www.{default}/'); 174 | assert.equal(route['primary'], true); 175 | }); 176 | 177 | it('returns all upstream routes', () => { 178 | const c = new psh.Config(mockEnvironmentRuntime); 179 | 180 | const routes = c.getUpstreamRoutes(); 181 | 182 | assert.equal(3, Object.keys(routes).length); 183 | assert.equal(routes['https://www.master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/']['original_url'], 'https://www.{default}/'); 184 | }); 185 | 186 | it('returns all upstream routes for a specific app', () => { 187 | const c = new psh.Config(mockEnvironmentRuntime); 188 | 189 | const routes = c.getUpstreamRoutes('app'); 190 | 191 | assert.equal(2, Object.keys(routes).length); 192 | assert.equal(routes['https://www.master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/']['original_url'], 'https://www.{default}/'); 193 | }); 194 | 195 | it('returns all upstream routes for a specific app on dedicated', () => { 196 | const env = mockEnvironmentRuntime; 197 | // Simulate a Dedicated-style upstream name. 198 | const routeData = loadJsonFile('PLATFORM_ROUTES'); 199 | routeData['https://www.master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/']['upstream'] = 'app:http'; 200 | env['PLATFORM_ROUTES'] = encode(routeData); 201 | 202 | const c = new psh.Config(env); 203 | 204 | const routes = c.getUpstreamRoutes('app'); 205 | 206 | assert.equal(2, Object.keys(routes).length); 207 | assert.equal(routes['https://www.master-7rqtwti-gcpjkefjk4wc2.us-2.platformsh.site/']['original_url'], 'https://www.{default}/'); 208 | }); 209 | 210 | it('gets a route by id', () => { 211 | const c = new psh.Config(mockEnvironmentRuntime); 212 | 213 | const route = c.getRoute('main'); 214 | 215 | assert.equal(route['original_url'], 'https://www.{default}/'); 216 | }); 217 | 218 | it('throws on a non-existant route id', () => { 219 | const c = new psh.Config(mockEnvironmentRuntime); 220 | 221 | assert.throws(() => { 222 | c.getRoute('missing'); 223 | }); 224 | }); 225 | 226 | 227 | it('loads all routes in local', () => { 228 | const env = deepClone(mockEnvironmentRuntime); 229 | delete env['PLATFORM_APPLICATION_NAME']; 230 | delete env['PLATFORM_ENVIRONMENT']; 231 | delete env['PLATFORM_BRANCH']; 232 | const c = new psh.Config(env); 233 | 234 | const routes = c.routes(); 235 | 236 | assert.ok(typeof routes == 'object'); 237 | assert.equal(Object.keys(routes).length, 6); 238 | }); 239 | }); 240 | 241 | describe("Relationship tests", () => { 242 | 243 | it('returns an existing relationship by name', () => { 244 | const c = new psh.Config(mockEnvironmentRuntime); 245 | 246 | const creds = c.credentials('database'); 247 | 248 | assert.equal(creds['scheme'], 'mysql'); 249 | assert.equal(creds['type'], 'mysql:10.2'); 250 | }); 251 | 252 | it('throws an exception for a missing relationship name', () => { 253 | const c = new psh.Config(mockEnvironmentRuntime); 254 | 255 | assert.throws(() => { 256 | const creds = c.getRoute('missing'); 257 | }); 258 | }); 259 | 260 | it('throws an exception for a missing relationship index', () => { 261 | const c = new psh.Config(mockEnvironmentRuntime); 262 | 263 | assert.throws(() => { 264 | const creds = c.getRoute('database', 3); 265 | }); 266 | }); 267 | }); 268 | 269 | describe('hasRelationship tests', () => { 270 | 271 | if('returns true for an existing relationship', () => { 272 | const c = new psh.Config(mockEnvironmentRuntime); 273 | 274 | assert.ok(c.hasRelationship('database')); 275 | }); 276 | 277 | if('returns false for an missing relationship', () => { 278 | const c = new psh.Config(mockEnvironmentRuntime); 279 | 280 | assert.ok(c.hasRelationship('missing')); 281 | }); 282 | }); 283 | 284 | describe("Variables tests", () => { 285 | 286 | it('returns an existing variable', () => { 287 | const c = new psh.Config(mockEnvironmentRuntime); 288 | 289 | const value = c.variable('somevar'); 290 | 291 | assert.equal(value, 'someval'); 292 | }); 293 | 294 | it('returns a default value when the variable doesn\'t exist', () => { 295 | const c = new psh.Config(mockEnvironmentRuntime); 296 | 297 | const value = c.variable('missing', 'default-val'); 298 | 299 | assert.equal(value, 'default-val'); 300 | }); 301 | 302 | it('returns all variables when on Platform', () => { 303 | const c = new psh.Config(mockEnvironmentRuntime); 304 | 305 | const value = c.variables(); 306 | 307 | assert.equal(value['somevar'], 'someval'); 308 | }); 309 | 310 | it('return a variable with a custom prefix', () => { 311 | const c = new psh.Config(mockEnvironmentRuntime, "CUSTOM_PREFIX_"); 312 | 313 | const withCustomPrefix = c._getValue("variable_with_custom_prefix"); 314 | const hasNoCustomPrefix = c._getValue("somevar"); 315 | 316 | assert.equal(withCustomPrefix, "with custom prefix"); 317 | assert.equal(hasNoCustomPrefix, null); 318 | }); 319 | }); 320 | 321 | describe("Application tests", () => { 322 | 323 | it('returns the application array on Platform.sh', () => { 324 | const c = new psh.Config(mockEnvironmentRuntime); 325 | 326 | const app = c.application(); 327 | 328 | assert.equal(app['type'], 'php:7.2'); 329 | }); 330 | }); 331 | 332 | 333 | describe("Raw property tests", () => { 334 | 335 | it('returns the correct value for raw properties', () => { 336 | const c = new psh.Config(mockEnvironmentRuntime); 337 | 338 | assert.equal(c.appDir, '/app'); 339 | assert.equal(c.applicationName, 'app'); 340 | assert.equal(c.project, 'test-project'); 341 | assert.equal(c.treeId, 'abc123'); 342 | assert.equal(c.projectEntropy, 'def789'); 343 | 344 | assert.equal(c.branch, 'feature-x'); 345 | assert.equal(c.environment, 'feature-x-hgi456'); 346 | assert.equal(c.documentRoot, '/app/web'); 347 | assert.equal(c.smtpHost, '1.2.3.4'); 348 | assert.equal(c.port, '8080'); 349 | assert.equal(c.socket, 'unix://tmp/blah.sock'); 350 | }); 351 | 352 | it('throws when a runtime property is accessed at build time', () => { 353 | const c = new psh.Config(mockEnvironmentBuild); 354 | 355 | assert.throws(() => { 356 | const branch = c.branch; 357 | }); 358 | }); 359 | }); 360 | 361 | describe('Credential formatter tests', () => { 362 | 363 | it('throws when a formatter is not found', () => { 364 | const c = new psh.Config(mockEnvironmentRuntime); 365 | 366 | assert.throws(() => { 367 | c.formattedCredentials('database', 'not-existing'); 368 | }); 369 | }); 370 | 371 | it('calls a registered formatter', () => { 372 | const c = new psh.Config(mockEnvironmentRuntime); 373 | let called = false; 374 | 375 | c.registerFormatter('test', (credentials) => { 376 | called = true; 377 | return 'stuff'; 378 | }); 379 | 380 | const formatted = c.formattedCredentials('database', 'test'); 381 | 382 | assert.ok(called); 383 | assert.equal(formatted, 'stuff'); 384 | }); 385 | 386 | it('formats a solr-node connection', () => { 387 | const c = new psh.Config(mockEnvironmentRuntime); 388 | 389 | const formatted = c.formattedCredentials('solr', 'solr-node'); 390 | 391 | assert.deepEqual(formatted, { 392 | host: 'solr.internal', 393 | port: 8080, 394 | protocol: 'http', 395 | core: 'collection1' 396 | }); 397 | }); 398 | 399 | it('formatts a puppeteer connection', () => { 400 | const c = new psh.Config(mockEnvironmentRuntime); 401 | 402 | const formatted = c.formattedCredentials('headless', 'puppeteer') 403 | 404 | assert.equal(formatted, 'http://169.254.16.215:9222') 405 | }); 406 | }); 407 | 408 | }); 409 | --------------------------------------------------------------------------------