UNPKG

24.7 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright © 2018 Atomist, Inc.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18Object.defineProperty(exports, "__esModule", { value: true });
19const appRoot = require("app-root-path");
20const cluster = require("cluster");
21const fs = require("fs-extra");
22const glob = require("glob");
23const stringify = require("json-stringify-safe");
24const _ = require("lodash");
25const p = require("path");
26const semver = require("semver");
27const globals_1 = require("./globals");
28const config_1 = require("./internal/util/config");
29const logger_1 = require("./internal/util/logger");
30const string_1 = require("./internal/util/string");
31const axiosHttpClient_1 = require("./spi/http/axiosHttpClient");
32const packageJson_1 = require("./util/packageJson");
33/**
34 * Generate defaults for various configuration option values. These
35 * will only be used if values are not provided by any source. Values
36 * not provided here will be `undefined`.
37 *
38 * @return default configuration
39 */
40function defaultConfiguration() {
41 const pj = packageJson_1.loadHostPackageJson() || {};
42 pj.name = pj.name || "atm-client-" + string_1.guid();
43 pj.version = pj.version || "0.0.0";
44 pj.keywords = pj.keywords || [];
45 const cfg = loadDefaultConfiguration();
46 cfg.name = pj.name;
47 cfg.version = pj.version;
48 cfg.keywords = pj.keywords;
49 cfg.application = pj.name.replace(/^@.*?\//, "");
50 return cfg;
51}
52exports.defaultConfiguration = defaultConfiguration;
53/**
54 * Exposes the configuration for lookup of configuration values.
55 * This is useful for components to obtain values eg. from configuration.custom
56 * like user provided secrets etc.
57 * @param {string} path the property path evaluated against the configuration instance
58 * @returns {T}
59 */
60function configurationValue(path, defaultValue) {
61 if (globals_1.automationClientInstance()) {
62 const conf = globals_1.automationClientInstance().configuration;
63 let value;
64 if (!path || path.length === 0) {
65 value = conf;
66 }
67 else {
68 value = _.get(conf, path);
69 }
70 if (value != null) {
71 return value;
72 }
73 else if (defaultValue !== undefined) {
74 return defaultValue;
75 }
76 }
77 else if (defaultValue) {
78 return defaultValue;
79 }
80 throw new Error(`Required @Value '${path}' not available`);
81}
82exports.configurationValue = configurationValue;
83/**
84 * Return the default configuration based on NODE_ENV or ATOMIST_ENV.
85 * ATOMIST_ENV takes precedence if it is set.
86 */
87function loadDefaultConfiguration() {
88 const cfg = exports.LocalDefaultConfiguration;
89 let envSpecificCfg = {};
90 const nodeEnv = process.env.ATOMIST_ENV || process.env.NODE_ENV;
91 if (nodeEnv === "production") {
92 envSpecificCfg = exports.ProductionDefaultConfiguration;
93 }
94 else if (nodeEnv === "staging" || nodeEnv === "testing") {
95 envSpecificCfg = exports.TestingDefaultConfiguration;
96 }
97 else if (nodeEnv) {
98 cfg.environment = nodeEnv;
99 }
100 return mergeConfigs(cfg, envSpecificCfg);
101}
102/**
103 * Return Atomist user configuration directory.
104 */
105function userConfigDir() {
106 const home = process.env[process.platform === "win32" ? "USERPROFILE" : "HOME"];
107 return p.join(home, ".atomist");
108}
109/**
110 * Return user automation client configuration path.
111 */
112function userConfigPath() {
113 const clientConfigFile = "client.config.json";
114 return p.join(userConfigDir(), clientConfigFile);
115}
116exports.userConfigPath = userConfigPath;
117/**
118 * Write user config securely, creating directories as necessary.
119 */
120function writeUserConfig(cfg) {
121 const cfgDir = userConfigDir();
122 return fs.ensureDir(cfgDir)
123 .then(() => fs.chmod(cfgDir, 0o700))
124 .then(() => fs.writeJson(userConfigPath(), cfg, {
125 spaces: 2,
126 encoding: "utf8",
127 mode: 0o600,
128 }));
129}
130exports.writeUserConfig = writeUserConfig;
131/**
132 * Read and return user config from UserConfigFile.
133 */
134function getUserConfig() {
135 if (fs.existsSync(userConfigPath())) {
136 try {
137 const cfg = fs.readJsonSync(userConfigPath());
138 // user config should not have name or version
139 if (cfg.name) {
140 delete cfg.name;
141 }
142 if (cfg.version) {
143 delete cfg.version;
144 }
145 return cfg;
146 }
147 catch (e) {
148 e.message = `Failed to read user config: ${e.message}`;
149 throw e;
150 }
151 }
152 return undefined;
153}
154exports.getUserConfig = getUserConfig;
155/**
156 * Log the loading of a configuration
157 *
158 * @param source name of configuration source
159 */
160function cfgLog(source) {
161 if (cluster.isMaster) {
162 logger_1.logger.debug(`Loading ${source} configuration`);
163 }
164}
165/**
166 * Overwrite values in the former configuration with values in the
167 * latter. The start object is modified.
168 *
169 * @param obj starting configuration
170 * @param override configuration values to add/override those in start
171 * @return resulting merged configuration
172 */
173function mergeConfigs(obj, ...sources) {
174 return _.mergeWith(obj, ...sources, (objValue, srcValue) => {
175 if (_.isArray(srcValue)) {
176 return srcValue;
177 }
178 });
179}
180exports.mergeConfigs = mergeConfigs;
181/**
182 * Merge a user's global and proper per-module configuration, if it
183 * exists. Values from the per-module configuration take precedence
184 * over the user-wide values. Per-module configuration is gotten from
185 * the first per-module configuration that matches name and,
186 * optionally, the version is within the per-module configuration's
187 * version range. A module configuration without a version range
188 * matches the named module with any version. If no version is
189 * provided, any version range is satisfied, meaning the first
190 * per-module configuration with a matching name is used. If no name
191 * is provide, only the user configuration is loaded. The first
192 * per-module match is used. This means if you have multiple
193 * configurations for the same named module and you want to include a
194 * default configuration for that module, put a configuration without
195 * a version range _after_ all the configurations with version ranges.
196 * Note that only values from the first per-module match are used.
197 *
198 * @param userConfig the user's configuration, which may include per-module configuration
199 * @param name automation client package name to load as module config if it exists
200 * @param version automation client package version to load as module config if
201 * version satifies module config version range
202 * @return the merged module and user configuration
203 */
204function resolveModuleConfig(userConfig, name, version) {
205 const cfg = {};
206 if (userConfig) {
207 cfgLog("user");
208 const uc = _.cloneDeep(userConfig);
209 let mc = {};
210 if (userConfig.modules) {
211 delete uc.modules;
212 if (name) {
213 let modCfg;
214 const moduleConfigs = userConfig.modules.filter(m => m.name === name);
215 if (version) {
216 modCfg = moduleConfigs.find(m => !m.version || semver.satisfies(version, m.version));
217 }
218 else if (moduleConfigs.length > 0) {
219 modCfg = moduleConfigs[0];
220 }
221 if (modCfg) {
222 cfgLog("module");
223 if (modCfg.name) {
224 delete modCfg.name;
225 }
226 if (modCfg.version) {
227 delete modCfg.version;
228 }
229 mc = modCfg;
230 }
231 }
232 }
233 mergeConfigs(cfg, uc, mc);
234 }
235 return cfg;
236}
237exports.resolveModuleConfig = resolveModuleConfig;
238/**
239 * Try to read user config, overriding its values with a per-module
240 * configuration that matches this automation.
241 *
242 * @param name automation client package name to load as module config if it exists
243 * @param version automation client package version to load as module config if
244 * version satifies module config version range
245 * @return module-specific config with user config supplying defaults
246 */
247function loadUserConfiguration(name, version) {
248 const userConfig = getUserConfig();
249 return resolveModuleConfig(userConfig, name, version);
250}
251exports.loadUserConfiguration = loadUserConfiguration;
252/**
253 * Load the automation configuration from the configuration object
254 * exported from cfgPath and return it. If no configuration path is
255 * provided, the package will be searched for a file named
256 * atomist.config.js. If no atomist.config.js is found, an empty
257 * object is returned. If more than one is found, an exception is
258 * thrown.
259 *
260 * @param cfgPath location of automation configuration
261 * @return automation configuration
262 */
263function loadAutomationConfig(cfgPath) {
264 let cfg = {};
265 if (!cfgPath) {
266 const cfgFile = "atomist.config.js";
267 const files = glob.sync(`${appRoot.path}/**/${cfgFile}`, { ignore: ["**/{.git,node_modules}/**"] });
268 if (files.length === 1) {
269 cfgPath = files[0];
270 }
271 else if (files.length > 1) {
272 throw new Error(`More than one automation configuration found in package: ${files.join(", ")}`);
273 }
274 }
275 if (cfgPath) {
276 try {
277 cfg = require(cfgPath).configuration;
278 cfgLog("automation config");
279 }
280 catch (e) {
281 e.message = `Failed to load ${cfgPath}.configuration: ${e.message}`;
282 throw e;
283 }
284 }
285 return cfg;
286}
287exports.loadAutomationConfig = loadAutomationConfig;
288/**
289 * Load configuration from the file defined by the ATOMIST_CONFIG_PATH
290 * environment variable, if it the variable is defined and the file
291 * exists, and return it. The contents of the ATOMIST_CONFIG_PATH
292 * file should be serialized JSON of AutomationServerOptions. If the
293 * environment variable is not defined or the file path specified by
294 * its value cannot be read as JSON, an empty object is returned.
295 *
296 * @return automation server options
297 */
298function loadAtomistConfigPath() {
299 let cfg = {};
300 if (process.env.ATOMIST_CONFIG_PATH) {
301 try {
302 cfg = fs.readJsonSync(process.env.ATOMIST_CONFIG_PATH);
303 cfgLog("ATOMIST_CONFIG_PATH");
304 }
305 catch (e) {
306 e.message = `Failed to read ATOMIST_CONFIG_PATH: ${e.message}`;
307 throw e;
308 }
309 }
310 return cfg;
311}
312exports.loadAtomistConfigPath = loadAtomistConfigPath;
313/**
314 * Load configuration from the ATOMIST_CONFIG environment variable, if
315 * it the variable is defined, and merge it into the passed in
316 * configuration. The value of the ATOMIST_CONFIG environment
317 * variable should be serialized JSON of AutomationServerOptions. The
318 * values from the environment variable will override values in the
319 * passed in configuration. If the environment variable is not
320 * defined, the passed in configuration is returned unchanged.
321 *
322 * @return automation server options
323 */
324function loadAtomistConfig() {
325 let cfg = {};
326 if (process.env.ATOMIST_CONFIG) {
327 try {
328 cfg = JSON.parse(process.env.ATOMIST_CONFIG);
329 cfgLog("ATOMIST_CONFIG");
330 }
331 catch (e) {
332 e.message = `Failed to parse contents of ATOMIST_CONFIG environment variable: ${e.message}`;
333 throw e;
334 }
335 }
336 return cfg;
337}
338exports.loadAtomistConfig = loadAtomistConfig;
339/**
340 * Examine environment, config, and cfg for Atomist workspace IDs.
341 * The ATOMIST_WORKSPACES environment variable takes precedence over
342 * the configuration "workspaceIds", which takes precedence over
343 * cfg.workspaceId, which may be undefined, null, or an empty array.
344 * If the ATOMIST_WORKSPACES environment variable is not set,
345 * workspaceIds is not set in config, and workspaceIds is falsey in
346 * cfg and teamIds is resolvable from the configuration, workspaceIds
347 * is set to teamIds.
348 *
349 * @param cfg current configuration, whose workspaceIds and teamIds
350 * properties may be modified by this function
351 * @return the resolved workspace IDs
352 */
353function resolveWorkspaceIds(cfg) {
354 if (process.env.ATOMIST_WORKSPACES) {
355 cfg.workspaceIds = process.env.ATOMIST_WORKSPACES.split(",");
356 }
357 else if (config_1.config("workspaceIds")) {
358 cfg.workspaceIds = config_1.config("workspaceIds");
359 }
360 return cfg.workspaceIds;
361}
362exports.resolveWorkspaceIds = resolveWorkspaceIds;
363/**
364 * Resolve a value from a environment variables or configuration keys.
365 * The environment variables are checked in order and take precedence
366 * over the configuration key, which are also checked in order. If
367 * no truthy values are found, undefined is returned.
368 *
369 * @param environmentVariables environment variables to check
370 * @param configKeyPaths configuration keys, as JSON paths, to check
371 * @param defaultValue value to use if no environment variables or config keys have values
372 * @return first truthy value found, or defaultValue
373 */
374function resolveConfigurationValue(environmentVariables, configKeyPaths, defaultValue) {
375 for (const ev of environmentVariables) {
376 if (process.env[ev]) {
377 return process.env[ev];
378 }
379 }
380 for (const cv of configKeyPaths) {
381 if (config_1.config(cv)) {
382 return config_1.config(cv);
383 }
384 }
385 return defaultValue;
386}
387exports.resolveConfigurationValue = resolveConfigurationValue;
388/**
389 * Resolve the HTTP port from the environment and configuration. The
390 * PORT environment variable takes precedence over the config value.
391 */
392function resolvePort(cfg) {
393 if (process.env.PORT) {
394 cfg.http.port = parseInt(process.env.PORT, 10);
395 }
396 return cfg.http.port;
397}
398exports.resolvePort = resolvePort;
399const EnvironmentVariablePrefix = "ATOMIST_";
400/**
401 * Resolve ATOMIST_ environment variables and add them to config.
402 * Variables of like ATOMIST_custom_foo_bar will be converted to
403 * a json path of custom.foo.bar.
404 * @param {Configuration} cfg
405 */
406function resolveEnvironmentVariables(cfg) {
407 for (const key in process.env) {
408 if (key.startsWith(EnvironmentVariablePrefix)
409 && process.env.hasOwnProperty(key)) {
410 const cleanKey = key.slice(EnvironmentVariablePrefix.length).split("_").join(".");
411 if (cleanKey[0] !== cleanKey[0].toUpperCase()) {
412 _.update(cfg, cleanKey, () => process.env[key]);
413 }
414 }
415 }
416}
417exports.resolveEnvironmentVariables = resolveEnvironmentVariables;
418/**
419 * Resolve placeholders against the process.env.
420 * Placeholders should be of form ${ENV_VAR}. Placeholders support default values
421 * in case they aren't defined: ${ENV_VAR:default value}
422 * @param {Configuration} config
423 */
424function resolvePlaceholders(cfg) {
425 resolvePlaceholdersRecursively(cfg);
426}
427exports.resolvePlaceholders = resolvePlaceholders;
428function resolvePlaceholdersRecursively(obj) {
429 for (const property in obj) {
430 if (obj.hasOwnProperty(property)) {
431 if (typeof obj[property] === "object") {
432 resolvePlaceholdersRecursively(obj[property]);
433 }
434 else if (typeof obj[property] === "string") {
435 obj[property] = resolvePlaceholder(obj[property]);
436 }
437 }
438 }
439}
440const PlaceholderExpression = /\$\{([.a-zA-Z_-]+)([.:0-9a-zA-Z-_ \" ]+)*\}/g;
441function resolvePlaceholder(value) {
442 if (PlaceholderExpression.test(value)) {
443 PlaceholderExpression.lastIndex = 0;
444 let result;
445 // tslint:disable-next-line:no-conditional-assignment
446 while (result = PlaceholderExpression.exec(value)) {
447 const fm = result[0];
448 const envValue = process.env[result[1]];
449 const defaultValue = result[2] ? result[2].trim().slice(1) : undefined;
450 if (envValue) {
451 value = value.split(fm).join(envValue);
452 }
453 else if (defaultValue) {
454 value = value.split(fm).join(defaultValue);
455 }
456 else {
457 throw new Error(`Environment variable '${result[1]}' is not defined`);
458 }
459 }
460 }
461 return value;
462}
463/**
464 * Invoke postProcessors on the provided configuration.
465 */
466function invokePostProcessors(cfg) {
467 return cfg.postProcessors.reduce((pp, fp) => pp.then(fp), Promise.resolve(cfg));
468}
469exports.invokePostProcessors = invokePostProcessors;
470/**
471 * Make sure final configuration has the minimum configuration it
472 * needs. It will throw an error if required properties are missing.
473 *
474 * @param cfg final configuration
475 */
476function validateConfiguration(cfg) {
477 if (!cfg) {
478 throw new Error(`no configuration defined`);
479 }
480 const errors = [];
481 if (!cfg.name) {
482 errors.push("you must set a 'name' property in your configuration");
483 }
484 if (!cfg.version) {
485 errors.push("you must set a 'version' property in your configuration");
486 }
487 if (!cfg.apiKey) {
488 console.info("INFO: To obtain an 'apiKey' visit https://app.atomist.com/apikeys and run 'atomist config' " +
489 "to configure the apiKey in your local configuration");
490 errors.push("you must set an 'apiKey' property in your configuration");
491 }
492 cfg.workspaceIds = cfg.workspaceIds || [];
493 cfg.groups = cfg.groups || [];
494 if (cfg.workspaceIds.length < 1 && cfg.groups.length < 1) {
495 errors.push("you must either provide an array of 'groups' in your configuration or, more likely, provide " +
496 "an array of 'workspaceIds' in your configuration or set the ATOMIST_WORKSPACES environment variable " +
497 "to a comma-separated list of workspace IDs");
498 }
499 if (cfg.workspaceIds.length > 0 && cfg.groups.length > 0) {
500 errors.push("you cannot specify both 'workspaceIds' and 'groups' in your configuration, you must set one " +
501 "to an empty array");
502 }
503 if (errors.length > 0) {
504 const msg = `Configuration (${stringify(cfg, string_1.obfuscateJson)}) is not correct: ${errors.join("; ")}`;
505 throw new Error(msg);
506 }
507}
508exports.validateConfiguration = validateConfiguration;
509/**
510 * Load and populate the automation configuration. The configuration
511 * is loaded from several locations with the following precedence from
512 * highest to lowest.
513 *
514 * 0. Recognized environment variables (see below)
515 * 1. The value of the ATOMIST_CONFIG environment variable, parsed as
516 * JSON and cast to AutomationServerOptions
517 * 2. The contents of the ATOMIST_CONFIG_PATH file as AutomationServerOptions
518 * 3. The automation's atomist.config.js exported configuration as
519 * Configuration
520 * 4. The contents of the user's client.config.json as UserConfig
521 * resolving user and per-module configuration into Configuration
522 * 5. ProductionDefaultConfiguration if ATOMIST_ENV or NODE_ENV is set
523 * to "production" or TestingDefaultConfiguration if ATOMIST_ENV or
524 * NODE_ENV is set to "staging" or "testing", with ATOMIST_ENV
525 * taking precedence over NODE_ENV.
526 * 6. LocalDefaultConfiguration
527 *
528 * If any of the sources are missing, they are ignored. Any truthy
529 * configuration values specified by sources of higher precedence
530 * cause any values provided by sources of lower precedence to be
531 * ignored. Arrays are replaced, not merged. Typically the only
532 * required values in the configuration for a successful registration
533 * are the apiKey and non-empty workspaceIds.
534 *
535 * Placeholder of the form `${ENV_VARIABLE}` in string configuration
536 * values will get resolved against the environment. The resolution
537 * happens at the very end when all configs have been merged.
538 *
539 * The configuration exported from the atomist.config.js is modified
540 * to contain the final configuration values and returned from this
541 * function.
542 *
543 * @param cfgPath path to file exporting the configuration object, if
544 * not provided the package is searched for one
545 * @return merged configuration object
546 */
547function loadConfiguration(cfgPath) {
548 // Register the logger globally so that downstream modules can see it
549 global.__logger = logger_1.logger;
550 let cfg;
551 try {
552 const defCfg = defaultConfiguration();
553 const userCfg = loadUserConfiguration(defCfg.name, defCfg.version);
554 const autoCfg = loadAutomationConfig(cfgPath);
555 const atmPathCfg = loadAtomistConfigPath();
556 const atmCfg = loadAtomistConfig();
557 cfg = mergeConfigs({}, defCfg, userCfg, autoCfg, atmPathCfg, atmCfg);
558 resolveWorkspaceIds(cfg);
559 resolvePort(cfg);
560 resolveEnvironmentVariables(cfg);
561 resolvePlaceholders(cfg);
562 }
563 catch (e) {
564 logger_1.logger.error(`Failed to load configuration: ${e.message}`);
565 if (e.stack) {
566 logger_1.logger.error(`Stack trace:\n${e.stack}`);
567 }
568 return Promise.reject(e);
569 }
570 return invokePostProcessors(cfg)
571 .then(completeCfg => {
572 completeCfg.postProcessors = [];
573 try {
574 validateConfiguration(completeCfg);
575 }
576 catch (e) {
577 return Promise.reject(e);
578 }
579 return Promise.resolve(completeCfg);
580 });
581}
582exports.loadConfiguration = loadConfiguration;
583/**
584 * Default configuration when running in neither testing or
585 * production.
586 */
587exports.LocalDefaultConfiguration = {
588 workspaceIds: [],
589 groups: [],
590 environment: "local",
591 policy: "ephemeral",
592 endpoints: {
593 api: "https://automation.atomist.com/registration",
594 graphql: "https://automation.atomist.com/graphql/team",
595 },
596 http: {
597 enabled: true,
598 host: "localhost",
599 auth: {
600 basic: {
601 enabled: false,
602 },
603 bearer: {
604 enabled: false,
605 },
606 },
607 customizers: [],
608 client: {
609 factory: axiosHttpClient_1.DefaultHttpClientFactory,
610 },
611 },
612 ws: {
613 enabled: true,
614 termination: {
615 graceful: false,
616 gracePeriod: 10000,
617 },
618 compress: false,
619 timeout: 10000,
620 },
621 applicationEvents: {
622 enabled: false,
623 },
624 cluster: {
625 enabled: false,
626 },
627 logging: {
628 level: "debug",
629 file: {
630 enabled: true,
631 level: "debug",
632 },
633 banner: {
634 enabled: true,
635 contributors: [],
636 },
637 },
638 statsd: {
639 enabled: false,
640 },
641 commands: null,
642 events: null,
643 ingesters: [],
644 listeners: [],
645 postProcessors: [],
646};
647/**
648 * Configuration defaults for production environments.
649 */
650exports.ProductionDefaultConfiguration = {
651 environment: "production",
652 policy: "durable",
653 http: {
654 port: 2866,
655 auth: {
656 basic: {
657 enabled: true,
658 },
659 bearer: {
660 enabled: true,
661 },
662 },
663 },
664 ws: {
665 termination: {
666 graceful: true,
667 },
668 compress: true,
669 },
670 applicationEvents: {
671 enabled: true,
672 },
673 cluster: {
674 enabled: true,
675 },
676 logging: {
677 level: "info",
678 file: {
679 enabled: false,
680 },
681 },
682 statsd: {
683 enabled: true,
684 },
685};
686/**
687 * Configuration defaults for pre-production environments.
688 */
689exports.TestingDefaultConfiguration = {
690 environment: "testing",
691 policy: "durable",
692 http: {
693 auth: {
694 basic: {
695 enabled: true,
696 },
697 bearer: {
698 enabled: true,
699 },
700 },
701 },
702 ws: {
703 termination: {
704 graceful: true,
705 },
706 compress: true,
707 },
708 applicationEvents: {
709 enabled: true,
710 },
711 cluster: {
712 enabled: true,
713 },
714 logging: {
715 level: "info",
716 file: {
717 enabled: false,
718 },
719 },
720 statsd: {
721 enabled: true,
722 },
723};
724//# sourceMappingURL=configuration.js.map
\No newline at end of file