1 | /**
|
2 | * Defines the MetaDeploymentManager class.
|
3 | *
|
4 | * @author Luke Chavers <luke@c2cschools.com>
|
5 | * @since 5.1.30
|
6 | * @license See LICENSE.md for details about licensing.
|
7 | * @copyright 2019 C2C Schools, LLC
|
8 | */
|
9 |
|
10 | ;
|
11 |
|
12 | const BaseGenerator = require( "../util/BaseGenerator" );
|
13 |
|
14 | /**
|
15 | * This class is used to manage metadata deployment during CI/CD build processes.
|
16 | *
|
17 | * @memberOf Deploy
|
18 | * @extends Util.BaseGenerator
|
19 | */
|
20 | class MetaDeploymentManager extends BaseGenerator {
|
21 |
|
22 | /**
|
23 | * This is the main entry point for execution of this meta
|
24 | * deployment manager.
|
25 | *
|
26 | * @access public
|
27 | * @returns {Promise}
|
28 | */
|
29 | execute() {
|
30 |
|
31 | // Locals
|
32 | let me = this;
|
33 |
|
34 | // Deps
|
35 | const _ = me.$dep("lodash");
|
36 | const bb = me.$dep("bluebird");
|
37 |
|
38 | // Tell someone...
|
39 | me.$log( "notice", "start", "The Metadata Deployment Manager is starting up ...");
|
40 |
|
41 | // Deploy to each target..
|
42 | return bb.mapSeries( me.modules,
|
43 |
|
44 | function( module, index ) {
|
45 |
|
46 | me.$log("info", "module.execute", "Deferring to Module: " + module.constructor.name );
|
47 | return module.execute();
|
48 |
|
49 | }
|
50 |
|
51 | ).then(
|
52 |
|
53 | function( allModuleResults ) {
|
54 |
|
55 | me.$log( "notice", "end", "Metadata deployment complete; exiting.");
|
56 |
|
57 | }
|
58 |
|
59 | );
|
60 |
|
61 | }
|
62 |
|
63 |
|
64 | //<editor-fold desc="--- Config Loading (deploy-config.yml) --------------">
|
65 |
|
66 |
|
67 |
|
68 |
|
69 | /**
|
70 | * Resolves the expected, absolute, path for the config/meta-deploy.yml file.
|
71 | *
|
72 | * @access public
|
73 | * @returns {string}
|
74 | */
|
75 | get deployConfigPath() {
|
76 |
|
77 | // Locals
|
78 | let me = this;
|
79 |
|
80 | // Deps
|
81 | const PATH = me.$dep("path");
|
82 |
|
83 | // Resolve the path and return it
|
84 | return PATH.join( me.serviceRootPath, "config/meta-deploy.yml" );
|
85 |
|
86 | }
|
87 |
|
88 | /**
|
89 | * Loads configuration data from config/meta-deploy.yml and returns the
|
90 | * full configuration as an object.
|
91 | *
|
92 | * @access public
|
93 | * @throws Error if the meta-deploy.yml file cannot be found or read.
|
94 | * @returns {object}
|
95 | */
|
96 | get deployConfig() {
|
97 |
|
98 | // Locals
|
99 | let me = this;
|
100 | let config;
|
101 |
|
102 | // Deps
|
103 | const TIPE = me.$dep("tipe");
|
104 |
|
105 | // Caching...
|
106 | if( me._deployConfig !== undefined ) {
|
107 | return me._deployConfig;
|
108 | }
|
109 |
|
110 | // Read meta-deploy.yml
|
111 | try {
|
112 | config = this.loadYamlFile( this.deployConfigPath );
|
113 | } catch( err ) {
|
114 | me.$throw("deploy.config.syntax.error", err, "Could not read 'config/meta-deploy.yml'" );
|
115 | }
|
116 |
|
117 | // Require that the meta-deploy.yml file has at least one item
|
118 | // defined in the "DeployTargets" property (array).
|
119 | if( config.DeployTargets === undefined || TIPE( config.DeployTargets ) !== "array" ||
|
120 | config.DeployTargets.length === 0 ) {
|
121 |
|
122 | me.$throw("deploy.config.no.targets", "MetaDeploymentManager requires at least one deployment target! (DeployTargets)" );
|
123 |
|
124 | }
|
125 |
|
126 | // Require that the meta-deploy.yml file has at least one item
|
127 | // defined in the "Modules" property (array).
|
128 | if( config.Modules === undefined || TIPE( config.Modules ) !== "array" ||
|
129 | config.Modules.length === 0 ) {
|
130 |
|
131 | me.$throw("deploy.config.no.modules", "MetaDeploymentManager requires at least one module to be defined! (Modules)" );
|
132 |
|
133 | }
|
134 |
|
135 | // Replace variables in config data
|
136 | config = me._processConfigVariables( config );
|
137 |
|
138 | // Cache the config
|
139 | me._deployConfig = config;
|
140 |
|
141 | // .. and return it..
|
142 | return config;
|
143 |
|
144 | }
|
145 |
|
146 |
|
147 |
|
148 |
|
149 | //</editor-fold>
|
150 |
|
151 | //<editor-fold desc="--- Config Variables --------------------------------">
|
152 |
|
153 |
|
154 |
|
155 |
|
156 | /**
|
157 | * Finds and reads the contents of the services' package.json file, with
|
158 | * caching.
|
159 | *
|
160 | * @access public
|
161 | * @throws Error if the package.json cannot be found, or read.
|
162 | * @returns {object}
|
163 | */
|
164 | get packageData() {
|
165 |
|
166 | // Locals
|
167 | let me = this;
|
168 |
|
169 | // Caching...
|
170 | if( me._packageData === undefined ) {
|
171 |
|
172 | // Depds
|
173 | const PATH = me.$dep("path");
|
174 |
|
175 | // Resolve the expected path to package.json
|
176 | let pkgPath = PATH.join( me.serviceRootPath, "package.json" );
|
177 |
|
178 | // Read package.json
|
179 | try {
|
180 | me._packageData = require( pkgPath );
|
181 | } catch( err ) {
|
182 | me.$throw("missing.package-json", "Failed to load the services' package.json file" );
|
183 | }
|
184 |
|
185 |
|
186 | }
|
187 |
|
188 | // All done..
|
189 | return me._packageData;
|
190 |
|
191 | }
|
192 |
|
193 | /**
|
194 | * Resolves the name of the service by looking at the contents of
|
195 | * package.json and returning the 'version' field (verbatim).
|
196 | *
|
197 | * @access public
|
198 | * @throws Error if the 'version' field cannot be found in the package.json file,
|
199 | * or if it appears to be invalid.
|
200 | * @returns {string}
|
201 | */
|
202 | get versionFull() {
|
203 |
|
204 | // Locals
|
205 | let me = this;
|
206 |
|
207 | // Deps
|
208 | const TIPE = me.$dep("tipe");
|
209 |
|
210 | // Load the contents of package.json
|
211 | let pkg = me.packageData;
|
212 |
|
213 | // Ensure that the 'version' property is provided and
|
214 | // appears to be valid.
|
215 | if( pkg.version === undefined || TIPE( pkg.version ) !== "string" ||
|
216 | pkg.version === "" ) {
|
217 |
|
218 | me.$throw("missing.version", "Missing or Invalid 'version' specified in package.json" );
|
219 |
|
220 | }
|
221 |
|
222 | // All done..
|
223 | return pkg.version;
|
224 |
|
225 | }
|
226 |
|
227 | /**
|
228 | * Resolves the MAJOR version of the service by looking at the contents of
|
229 | * package.json and parsing the first number in the 'version' property.
|
230 | *
|
231 | * @access public
|
232 | * @throws Error if the major version cannot be resolved.
|
233 | * @returns {string}
|
234 | */
|
235 | get versionMajor() {
|
236 |
|
237 | let me = this;
|
238 | let fv = me.versionFull;
|
239 | let spl = fv.split(".");
|
240 |
|
241 | if( spl.length === 0 ) {
|
242 | me.$throw("invalid.version", "Could not resolve service major version from package.json" );
|
243 | }
|
244 |
|
245 | return spl[0];
|
246 |
|
247 | }
|
248 |
|
249 | /**
|
250 | * Resolves the MINOR version of the service by looking at the contents of
|
251 | * package.json and parsing the second number in the 'version' property.
|
252 | *
|
253 | * Note: If the MINOR version cannot be resolved, this function will return
|
254 | * the literal string "0" (zero).
|
255 | *
|
256 | * @access public
|
257 | * @returns {string}
|
258 | */
|
259 | get versionMinor() {
|
260 |
|
261 | // Locals
|
262 | let me = this;
|
263 |
|
264 | // Fetch the full version string
|
265 | let fv = me.versionFull;
|
266 |
|
267 | // Split the full version string
|
268 | let spl = fv.split(".");
|
269 |
|
270 | // Check for our MINOR version
|
271 | if( spl.length < 2 ) {
|
272 |
|
273 | // It could not be found, return "0"
|
274 | return "0";
|
275 |
|
276 | } else {
|
277 |
|
278 | // Found the MINOR version, return it as a string..
|
279 | return spl[1];
|
280 |
|
281 | }
|
282 |
|
283 | }
|
284 |
|
285 | /**
|
286 | * Resolves the REVISION version of the service by looking at the contents of
|
287 | * package.json and parsing the third number in the 'version' property.
|
288 | *
|
289 | * Note: If the REVISION version cannot be resolved, this function will return
|
290 | * the literal string "0" (zero).
|
291 | *
|
292 | * @access public
|
293 | * @returns {string}
|
294 | */
|
295 | get versionRevision() {
|
296 |
|
297 | // Locals
|
298 | let me = this;
|
299 |
|
300 | // Fetch the full version string
|
301 | let fv = me.versionFull;
|
302 |
|
303 | // Split the full version string
|
304 | let spl = fv.split(".");
|
305 |
|
306 | // Check for our REVISION version
|
307 | if( spl.length < 3 ) {
|
308 |
|
309 | // It could not be found, return "0"
|
310 | return "0";
|
311 |
|
312 | } else {
|
313 |
|
314 | // Found the REVISION version, return it as a string..
|
315 | return spl[2];
|
316 |
|
317 | }
|
318 |
|
319 | }
|
320 |
|
321 | /**
|
322 | * Resolves the name of the service by looking at the contents of
|
323 | * package.json and returning the 'name' field.
|
324 | *
|
325 | * @access public
|
326 | * @throws Error if the 'name' field cannot be found in the package.json file,
|
327 | * or if it appears to be invalid.
|
328 | * @returns {string}
|
329 | */
|
330 | get serviceName() {
|
331 |
|
332 | // Locals
|
333 | let me = this;
|
334 |
|
335 | // Deps
|
336 | const TIPE = me.$dep("tipe");
|
337 |
|
338 | // Fetch the package.json contents..
|
339 | let pkg = me.packageData;
|
340 |
|
341 | // Ensure we have a 'name' field
|
342 | if( pkg.name === undefined || TIPE( pkg.name ) !== "string" ||
|
343 | pkg.name === "" ) {
|
344 |
|
345 | me.$throw("missing.service.name", "Missing or Invalid service 'name' specified in package.json" );
|
346 |
|
347 | }
|
348 |
|
349 | return pkg.name;
|
350 |
|
351 | }
|
352 |
|
353 | /**
|
354 | * Resolves the current Git branch by looking for and reading the
|
355 | * local .git/HEAD file.
|
356 | *
|
357 | * @access public
|
358 | * @throws Error If the file could not be found, read, or if the contents
|
359 | * are not recognized as valid.
|
360 | * @returns {string}
|
361 | */
|
362 | get gitBranch() {
|
363 |
|
364 | // Locals
|
365 | let me = this;
|
366 |
|
367 | // Dependencies
|
368 | const FS = me.$dep("fs");
|
369 | const PATH = me.$dep("path");
|
370 | const _ = me.$dep("lodash");
|
371 |
|
372 | // Resolve the expected path to .git/HEAD
|
373 | let gitHeadPath = PATH.join( me.serviceRootPath, ".git/HEAD" );
|
374 | let gitHeadContents, gitHeadSplit, gitBranch;
|
375 |
|
376 | // Read the .git/HEAD file
|
377 | try {
|
378 | gitHeadContents = FS.readFileSync( gitHeadPath, "utf8");
|
379 | } catch( err ) {
|
380 | me.$throw("missing.git.head", "Missing .git/HEAD file, which is required to resolve the current Git branch" );
|
381 | }
|
382 |
|
383 | // Parse out invalid characters
|
384 | gitHeadContents = gitHeadContents.replace( /[^a-zA-Z0-9\-\:\/\.]+/g, "" );
|
385 |
|
386 | // Simple check for validity (this is not very comprehensive)
|
387 | if( !_.startsWith( gitHeadContents, "ref:" ) || gitHeadContents.indexOf("/") === -1 ) {
|
388 | me.$throw("invalid.git.head", "Could not resolve the current Git Branch, the contents of .git/HEAD were not recognized." );
|
389 | }
|
390 |
|
391 | // Reduce the contents of .git/HEAD to a simple branch name..
|
392 | gitHeadSplit = gitHeadContents.split("/" );
|
393 | gitBranch = gitHeadSplit[ ( gitHeadSplit.length - 1 ) ];
|
394 |
|
395 | // All done, return the branch name..
|
396 | return gitBranch;
|
397 |
|
398 | }
|
399 |
|
400 | /**
|
401 | * Replaces variables (e.g. "${serviceName}") in the metadata deployment
|
402 | * configuration for the service ("config/meta-deploy.yml").
|
403 | *
|
404 | * @param config
|
405 | * @returns {{}}
|
406 | * @private
|
407 | */
|
408 | _processConfigVariables( config ) {
|
409 |
|
410 | // Locals
|
411 | let me = this;
|
412 |
|
413 | let vars = {
|
414 | versionMajor: me.versionMajor,
|
415 | versionMinor: me.versionMinor,
|
416 | versionRevision: me.versionRevision,
|
417 | versionFull: me.versionFull,
|
418 | serviceName: me.serviceName,
|
419 | gitBranch: me.gitBranch
|
420 | };
|
421 |
|
422 |
|
423 |
|
424 | return me._parseObjectForVariables( config, vars );
|
425 |
|
426 | }
|
427 |
|
428 |
|
429 |
|
430 |
|
431 | //</editor-fold>
|
432 |
|
433 | //<editor-fold desc="--- Modules -----------------------------------------">
|
434 |
|
435 |
|
436 |
|
437 |
|
438 | /**
|
439 | * Returns the 'Modules' property from the `deployConfig` object, after
|
440 | * a bit of parsing.
|
441 | *
|
442 | * @see deployConfig
|
443 | * @returns {array}
|
444 | */
|
445 | get moduleConfigs() {
|
446 |
|
447 | // Locals
|
448 | let me = this;
|
449 | let moduleConfig;
|
450 |
|
451 | // Deps
|
452 | const _ = me.$dep("lodash");
|
453 |
|
454 | // Caching
|
455 | if( me._moduleConfigs === undefined ) {
|
456 |
|
457 | // Get the full deploy config
|
458 | let config = me.deployConfig;
|
459 |
|
460 | // Init Cache Object
|
461 | me._moduleConfigs = [];
|
462 |
|
463 | // Copy enabled modules (include: true) over to the
|
464 | // internal module config cache object..
|
465 | _.each( config.Modules, function( module, index ) {
|
466 |
|
467 | if( module.include !== undefined && module.include === true ) {
|
468 |
|
469 | // We no longer need this property...
|
470 | delete module.include;
|
471 |
|
472 | // Copy it over..
|
473 | me._moduleConfigs.push( module );
|
474 |
|
475 | } else {
|
476 |
|
477 | if( module.type !== undefined ) {
|
478 |
|
479 | me.$log("warning", "module.disabled", "An instance of a '" + module.type + "' deployment module is disabled and will not be executed!");
|
480 |
|
481 | }
|
482 |
|
483 | }
|
484 |
|
485 | });
|
486 |
|
487 | }
|
488 |
|
489 | // All done..
|
490 | return me._moduleConfigs;
|
491 |
|
492 | }
|
493 |
|
494 | /**
|
495 | * Instantiates (as needed) and returns all of the "Module" helper
|
496 | * objects (children of ./module/BaseMetaDeployModule) based on the provided
|
497 | * `Modules` configuration in meta-deploy.yml.
|
498 | *
|
499 | * @access public
|
500 | * @returns {BaseMetaDeployModule[]}
|
501 | */
|
502 | get modules() {
|
503 |
|
504 | // Locals
|
505 | let me = this;
|
506 |
|
507 | // Deps
|
508 | const _ = me.$dep("lodash");
|
509 |
|
510 | // Caching
|
511 | if( me._modules !== undefined ) {
|
512 | return me._modules;
|
513 | }
|
514 |
|
515 | // Tell someone ...
|
516 | me.$log("info", "module.init", "Initializing Modules ..." );
|
517 |
|
518 | // Get the module config..
|
519 | let moduleConfigs = me.moduleConfigs;
|
520 |
|
521 | // Init cache object
|
522 | me._modules = [];
|
523 |
|
524 | // Instantiate each module
|
525 | _.each( moduleConfigs, function( cfg ) {
|
526 |
|
527 | // Extract the module's class name
|
528 | let className = cfg.type;
|
529 | delete cfg.type;
|
530 |
|
531 | // Attach this class to the config
|
532 | cfg.metaDeploymentManager = me;
|
533 |
|
534 | // Attach the deploy targets to the config
|
535 | cfg.targets = me.targets;
|
536 |
|
537 | // Instantiate the module class/object
|
538 | let mod = me.$spawn( "microservicesLib", "deploy/module/" + className, cfg );
|
539 |
|
540 | // Persist the target class/object
|
541 | me._modules.push( mod );
|
542 |
|
543 | });
|
544 |
|
545 | // All done
|
546 | return me._modules;
|
547 |
|
548 | }
|
549 |
|
550 |
|
551 |
|
552 |
|
553 | //</editor-fold>
|
554 |
|
555 | //<editor-fold desc="--- Targets -----------------------------------------">
|
556 |
|
557 |
|
558 |
|
559 |
|
560 | /**
|
561 | * Returns the 'DeployTargets' property from the `deployConfig` object,
|
562 | * after a bit of parsing.
|
563 | *
|
564 | * @access public
|
565 | * @see deployConfig
|
566 | * @returns {array}
|
567 | */
|
568 | get targetConfigs() {
|
569 | return this.deployConfig.DeployTargets;
|
570 | }
|
571 |
|
572 | /**
|
573 | * Instantiates (as needed) and returns all of the "Deploy Target" helper
|
574 | * objects (children of ./target/BaseMetaDeployTarget) based on the provided
|
575 | * `DeployTargets` configuration in meta-deploy.yml.
|
576 | *
|
577 | * @access public
|
578 | * @returns {BaseMetaDeployTarget[]}
|
579 | */
|
580 | get targets() {
|
581 |
|
582 | // Locals
|
583 | let me = this;
|
584 |
|
585 | // Deps
|
586 | const _ = me.$dep("lodash");
|
587 |
|
588 | // Caching
|
589 | if( me._targets !== undefined ) {
|
590 | return me._targets;
|
591 | }
|
592 |
|
593 | // Tell someone ...
|
594 | me.$log("info", "target.init", "Initializing Deploy Targets ..." );
|
595 |
|
596 | // Get the target configs..
|
597 | let targetConfigs = me.targetConfigs;
|
598 |
|
599 | // Init cache object
|
600 | me._targets = [];
|
601 |
|
602 | // Instantiate each target
|
603 | _.each( targetConfigs, function( cfg ) {
|
604 |
|
605 | // Extract the target's class name
|
606 | let className = cfg.type;
|
607 | delete cfg.type;
|
608 |
|
609 | // Attach this class to the config
|
610 | cfg.metaDeploymentManager = me;
|
611 |
|
612 | // Instantiate the target class/object
|
613 | let target = me.$spawn( "microservicesLib", "deploy/target/" + className, cfg );
|
614 |
|
615 | // Persist the target class/object
|
616 | me._targets.push( target );
|
617 |
|
618 | });
|
619 |
|
620 | // All done
|
621 | return me._targets;
|
622 |
|
623 | }
|
624 |
|
625 |
|
626 |
|
627 |
|
628 | //</editor-fold>
|
629 |
|
630 | }
|
631 |
|
632 | module.exports = MetaDeploymentManager;
|
633 |
|
634 |
|
635 | /*
|
636 |
|
637 | // Dependencies
|
638 | const YAML = me.$dep( "js-yaml" );
|
639 | const FS = me.$dep( "fs" );
|
640 |
|
641 | ================================================================================
|
642 |
|
643 | static loadYamlFile( absPath, property ) {
|
644 | non-static: loadYamlFile( absPath, property ) {
|
645 |
|
646 | ================================================================================
|
647 |
|
648 | // Create a "DebugHelper"...
|
649 | let deb = me.$spawn( "commonLib", "util/DebugHelper" );
|
650 |
|
651 | // Dump the config data
|
652 | deb.dbg( resolvedConfig, true, 1 );
|
653 |
|
654 | // ---- alt
|
655 |
|
656 | get $debugger() {
|
657 | $inspect( varToInspect, output, indent, title )
|
658 |
|
659 | ================================================================================
|
660 |
|
661 | return me._loadCommonConfigFile( "provider.yml", "provider" );
|
662 |
|
663 | ================================================================================
|
664 |
|
665 | me.$log(
|
666 | "trace",
|
667 | "Instantiating class ('" + name + "')"
|
668 | );
|
669 |
|
670 | ================================================================================
|
671 |
|
672 | _getDirContentsRecursive( absPathToRead, initialStore )
|
673 |
|
674 | ================================================================================
|
675 |
|
676 | _parseStringForVariables( str, vars ) {
|
677 |
|
678 | */ |
\ | No newline at end of file |