From 389e2ce24965c4466cb7006c95588c6b1d4bc425 Mon Sep 17 00:00:00 2001 From: Tristan Foureur Date: Mon, 11 Jul 2016 16:11:35 -0700 Subject: [PATCH] Initial Commit --- .gitignore | 1 + LICENSE.md | 596 +++++++++++++++++++++++++++++++++++++++ README.md | 107 +++++++ bin/opsworks | 107 +++++++ config/config.yml | 8 + img/prompt.png | Bin 0 -> 26212 bytes lib/Opsworks.js | 340 ++++++++++++++++++++++ lib/StackList.js | 87 ++++++ lib/commandrunner.js | 232 +++++++++++++++ lib/config.js | 5 + lib/hierarchy.js | 196 +++++++++++++ lib/logger.js | 17 ++ lib/prompt.js | 201 +++++++++++++ package.json | 31 ++ test/data/apps.json | 52 ++++ test/data/elbs.json | 26 ++ test/data/healthELB.json | 28 ++ test/data/layers.json | 144 ++++++++++ test/data/stacks.json | 104 +++++++ test/filters.js | 131 +++++++++ test/opsworks.js | 268 ++++++++++++++++++ 21 files changed, 2681 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100755 bin/opsworks create mode 100644 config/config.yml create mode 100644 img/prompt.png create mode 100644 lib/Opsworks.js create mode 100644 lib/StackList.js create mode 100644 lib/commandrunner.js create mode 100644 lib/config.js create mode 100644 lib/hierarchy.js create mode 100644 lib/logger.js create mode 100644 lib/prompt.js create mode 100644 package.json create mode 100644 test/data/apps.json create mode 100644 test/data/elbs.json create mode 100644 test/data/healthELB.json create mode 100644 test/data/layers.json create mode 100644 test/data/stacks.json create mode 100644 test/filters.js create mode 100644 test/opsworks.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d87b1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/* diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..16d89e0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,596 @@ +GNU GENERAL PUBLIC LICENSE +========================== + +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. <> + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +## Preamble + +The GNU General Public License is a free, copyleft license for software and other +kinds of works. + +The licenses for most software and other practical works are designed to take away +your freedom to share and change the works. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change all versions of a +program--to make sure it remains free software for all its users. We, the Free +Software Foundation, use the GNU General Public License for most of our software; it +applies also to any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General +Public Licenses are designed to make sure that you have the freedom to distribute +copies of free software (and charge for them if you wish), that you receive source +code or can get it if you want it, that you can change the software or use pieces of +it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or +asking you to surrender the rights. Therefore, you have certain responsibilities if +you distribute copies of the software, or if you modify it: responsibilities to +respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, +you must pass on to the recipients the same freedoms that you received. You must make +sure that they, too, receive or can get the source code. And you must show them these +terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal permission +to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is +no warranty for this free software. For both users' and authors' sake, the GPL +requires that modified versions be marked as changed, so that their problems will not +be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of +the software inside them, although the manufacturer can do so. This is fundamentally +incompatible with the aim of protecting users' freedom to change the software. The +systematic pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we have designed +this version of the GPL to prohibit the practice for those products. If such problems +arise substantially in other domains, we stand ready to extend this provision to +those domains in future versions of the GPL, as needed to protect the freedom of +users. + +Finally, every program is threatened constantly by software patents. States should +not allow patents to restrict development and use of software on general-purpose +computers, but in those that do, we wish to avoid the special danger that patents +applied to a free program could make it effectively proprietary. To prevent this, the +GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +## TERMS AND CONDITIONS + +### 0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in +a fashion requiring copyright permission, other than the making of an exact copy. The +resulting work is called a “modified version” of the earlier work or a +work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on +the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for infringement under +applicable copyright law, except executing it on a computer or modifying a private +copy. Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the +extent that it includes a convenient and prominently visible feature that (1) +displays an appropriate copyright notice, and (2) tells the user that there is no +warranty for the work (except to the extent that warranties are provided), that +licensees may convey the work under this License, and how to view a copy of this +License. If the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code. + +The “source code” for a work means the preferred form of the work for +making modifications to it. “Object code” means any non-source form of a +work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of interfaces +specified for a particular programming language, one that is widely used among +developers working in that language. + +The “System Libraries” of an executable work include anything, other than +the work as a whole, that (a) is included in the normal form of packaging a Major +Component, but which is not part of that Major Component, and (b) serves only to +enable use of the work with that Major Component, or to implement a Standard +Interface for which an implementation is available to the public in source code form. +A “Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system (if any) on which +the executable work runs, or a compiler used to produce the work, or an object code +interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the +source code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. However, +it does not include the work's System Libraries, or general-purpose tools or +generally available free programs which are used unmodified in performing those +activities but which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for the work, and +the source code for shared libraries and dynamically linked subprograms that the work +is specifically designed to require, such as by intimate data communication or +control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +### 2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright on the +Program, and are irrevocable provided the stated conditions are met. This License +explicitly affirms your unlimited permission to run the unmodified Program. The +output from running a covered work is covered by this License only if the output, +given its content, constitutes a covered work. This License acknowledges your rights +of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey covered +works to others for the sole purpose of having them make modifications exclusively +for you, or provide you with facilities for running those works, provided that you +comply with the terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for you must do so +exclusively on your behalf, under your direction and control, on terms that prohibit +them from making any copies of your copyrighted material outside their relationship +with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure under any +applicable law fulfilling obligations under article 11 of the WIPO copyright treaty +adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention +of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of +technological measures to the extent such circumvention is effected by exercising +rights under this License with respect to the covered work, and you disclaim any +intention to limit operation or modification of the work as a means of enforcing, +against the work's users, your or third parties' legal rights to forbid circumvention +of technological measures. + +### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you receive it, in any +medium, provided that you conspicuously and appropriately publish on each copy an +appropriate copyright notice; keep intact all notices stating that this License and +any non-permissive terms added in accord with section 7 apply to the code; keep +intact all notices of the absence of any warranty; and give all recipients a copy of +this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer +support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce it from +the Program, in the form of source code under the terms of section 4, provided that +you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified it, and giving a +relevant date. +* **b)** The work must carry prominent notices stating that it is released under this +License and any conditions added under section 7. This requirement modifies the +requirement in section 4 to “keep intact all notices”. +* **c)** You must license the entire work, as a whole, under this License to anyone who +comes into possession of a copy. This License will therefore apply, along with any +applicable section 7 additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no permission to license the +work in any other way, but it does not invalidate such permission if you have +separately received it. +* **d)** If the work has interactive user interfaces, each must display Appropriate Legal +Notices; however, if the Program has interactive interfaces that do not display +Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are +not by their nature extensions of the covered work, and which are not combined with +it such as to form a larger program, in or on a volume of a storage or distribution +medium, is called an “aggregate” if the compilation and its resulting +copyright are not used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work in an aggregate +does not cause this License to apply to the other parts of the aggregate. + +### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections 4 and +5, provided that you also convey the machine-readable Corresponding Source under the +terms of this License, in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product (including a +physical distribution medium), accompanied by the Corresponding Source fixed on a +durable physical medium customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product (including a +physical distribution medium), accompanied by a written offer, valid for at least +three years and valid for as long as you offer spare parts or customer support for +that product model, to give anyone who possesses the object code either (1) a copy of +the Corresponding Source for all the software in the product that is covered by this +License, on a durable physical medium customarily used for software interchange, for +a price no more than your reasonable cost of physically performing this conveying of +source, or (2) access to copy the Corresponding Source from a network server at no +charge. +* **c)** Convey individual copies of the object code with a copy of the written offer to +provide the Corresponding Source. This alternative is allowed only occasionally and +noncommercially, and only if you received the object code with such an offer, in +accord with subsection 6b. +* **d)** Convey the object code by offering access from a designated place (gratis or for +a charge), and offer equivalent access to the Corresponding Source in the same way +through the same place at no further charge. You need not require recipients to copy +the Corresponding Source along with the object code. If the place to copy the object +code is a network server, the Corresponding Source may be on a different server +(operated by you or a third party) that supports equivalent copying facilities, +provided you maintain clear directions next to the object code saying where to find +the Corresponding Source. Regardless of what server hosts the Corresponding Source, +you remain obligated to ensure that it is available for as long as needed to satisfy +these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided you inform +other peers where the object code and Corresponding Source of the work are being +offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the +Corresponding Source as a System Library, need not be included in conveying the +object code work. + +A “User Product” is either (1) a “consumer product”, which +means any tangible personal property which is normally used for personal, family, or +household purposes, or (2) anything designed or sold for incorporation into a +dwelling. In determining whether a product is a consumer product, doubtful cases +shall be resolved in favor of coverage. For a particular product received by a +particular user, “normally used” refers to a typical or common use of +that class of product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected to use, the +product. A product is a consumer product regardless of whether the product has +substantial commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install and execute +modified versions of a covered work in that User Product from a modified version of +its Corresponding Source. The information must suffice to ensure that the continued +functioning of the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for +use in, a User Product, and the conveying occurs as part of a transaction in which +the right of possession and use of the User Product is transferred to the recipient +in perpetuity or for a fixed term (regardless of how the transaction is +characterized), the Corresponding Source conveyed under this section must be +accompanied by the Installation Information. But this requirement does not apply if +neither you nor any third party retains the ability to install modified object code +on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to +continue to provide support service, warranty, or updates for a work that has been +modified or installed by the recipient, or for the User Product in which it has been +modified or installed. Access to a network may be denied when the modification itself +materially and adversely affects the operation of the network or violates the rules +and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with +this section must be in a format that is publicly documented (and with an +implementation available to the public in source code form), and must require no +special password or key for unpacking, reading or copying. + +### 7. Additional Terms. + +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. Additional +permissions that are applicable to the entire Program shall be treated as though they +were included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part may be +used separately under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when you +modify the work.) You may place additional permissions on material, added by you to a +covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a +covered work, you may (if authorized by the copyright holders of that material) +supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the terms of +sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or author +attributions in that material or in the Appropriate Legal Notices displayed by works +containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or requiring that +modified versions of such material be marked in reasonable ways as different from the +original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or authors of the +material; or +* **e)** Declining to grant rights under trademark law for use of some trade names, +trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that material by anyone +who conveys the material (or modified versions of it) with contractual assumptions of +liability to the recipient, for any liability that these contractual assumptions +directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you received +it, or any part of it, contains a notice stating that it is governed by this License +along with a term that is a further restriction, you may remove that term. If a +license document contains a further restriction but permits relicensing or conveying +under this License, you may add to a covered work material governed by the terms of +that license document, provided that the further restriction does not survive such +relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in +the relevant source files, a statement of the additional terms that apply to those +files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a +separately written license, or stated as exceptions; the above requirements apply +either way. + +### 8. Termination. + +You may not propagate or modify a covered work except as expressly provided under +this License. Any attempt otherwise to propagate or modify it is void, and will +automatically terminate your rights under this License (including any patent licenses +granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a +particular copyright holder is reinstated (a) provisionally, unless and until the +copyright holder explicitly and finally terminates your license, and (b) permanently, +if the copyright holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently +if the copyright holder notifies you of the violation by some reasonable means, this +is the first time you have received notice of violation of this License (for any +work) from that copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of +parties who have received copies or rights from you under this License. If your +rights have been terminated and not permanently reinstated, you do not qualify to +receive new licenses for the same material under section 10. + +### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of the +Program. Ancillary propagation of a covered work occurring solely as a consequence of +using peer-to-peer transmission to receive a copy likewise does not require +acceptance. However, nothing other than this License grants you permission to +propagate or modify any covered work. These actions infringe copyright if you do not +accept this License. Therefore, by modifying or propagating a covered work, you +indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a license +from the original licensors, to run, modify and propagate that work, subject to this +License. You are not responsible for enforcing compliance by third parties with this +License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an organization, or +merging organizations. If propagation of a covered work results from an entity +transaction, each party to that transaction who receives a copy of the work also +receives whatever licenses to the work the party's predecessor in interest had or +could give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if the predecessor +has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or +affirmed under this License. For example, you may not impose a license fee, royalty, +or other charge for exercise of rights granted under this License, and you may not +initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging +that any patent claim is infringed by making, using, selling, offering for sale, or +importing the Program or any portion of it. + +### 11. Patents. + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The work thus +licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or +controlled by the contributor, whether already acquired or hereafter acquired, that +would be infringed by some manner, permitted by this License, of making, using, or +selling its contributor version, but do not include claims that would be infringed +only as a consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant patent +sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license +under the contributor's essential patent claims, to make, use, sell, offer for sale, +import and otherwise run, modify and propagate the contents of its contributor +version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent (such as an +express permission to practice a patent or covenant not to sue for patent +infringement). To “grant” such a patent license to a party means to make +such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free of charge +and under the terms of this License, through a publicly available network server or +other readily accessible means, then you must either (1) cause the Corresponding +Source to be so available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner consistent with +the requirements of this License, to extend the patent license to downstream +recipients. “Knowingly relying” means you have actual knowledge that, but +for the patent license, your conveying the covered work in a country, or your +recipient's use of the covered work in a country, would infringe one or more +identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you +convey, or propagate by procuring conveyance of, a covered work, and grant a patent +license to some of the parties receiving the covered work authorizing them to use, +propagate, modify or convey a specific copy of the covered work, then the patent +license you grant is automatically extended to all recipients of the covered work and +works based on it. + +A patent license is “discriminatory” if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on the +non-exercise of one or more of the rights that are specifically granted under this +License. You may not convey a covered work if you are a party to an arrangement with +a third party that is in the business of distributing software, under which you make +payment to the third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties who would receive +the covered work from you, a discriminatory patent license (a) in connection with +copies of the covered work conveyed by you (or copies made from those copies), or (b) +primarily for and in connection with specific products or compilations that contain +the covered work, unless you entered into that arrangement, or that patent license +was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available to you +under applicable patent law. + +### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) +that contradict the conditions of this License, they do not excuse you from the +conditions of this License. If you cannot convey a covered work so as to satisfy +simultaneously your obligations under this License and any other pertinent +obligations, then as a consequence you may not convey it at all. For example, if you +agree to terms that obligate you to collect a royalty for further conveying from +those to whom you convey the Program, the only way you could satisfy both those terms +and this License would be to refrain entirely from conveying the Program. + +### 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have permission to link or +combine any covered work with a work licensed under version 3 of the GNU Affero +General Public License into a single combined work, and to convey the resulting work. +The terms of this License will continue to apply to the part which is the covered +work, but the special requirements of the GNU Affero General Public License, section +13, concerning interaction through a network will apply to the combination as such. + +### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU +General Public License from time to time. Such new versions will be similar in spirit +to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that +a certain numbered version of the GNU General Public License “or any later +version” applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published by the +Free Software Foundation. If the Program does not specify a version number of the GNU +General Public License, you may choose any version ever published by the Free +Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU +General Public License can be used, that proxy's public statement of acceptance of a +version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no +additional obligations are imposed on any author or copyright holder as a result of +your choosing to follow a later version. + +### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE +QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY +COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS +PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE +OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE +WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot be +given local legal effect according to their terms, reviewing courts shall apply local +law that most closely approximates an absolute waiver of all civil liability in +connection with the Program, unless a warranty or assumption of liability accompanies +a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +## How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to +the public, the best way to achieve this is to make it free software which everyone +can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them +to the start of each source file to most effectively state the exclusion of warranty; +and each file should have at least the “copyright” line and a pointer to +where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this +when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type 'show c' for details. + +The hypothetical commands 'show w' and 'show c' should show the appropriate parts of +the General Public License. Of course, your program's commands might be different; +for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to +sign a “copyright disclaimer” for the program, if necessary. For more +information on this, and how to apply and follow the GNU GPL, see +<>. + +The GNU General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may consider it +more useful to permit linking proprietary applications with the library. If this is +what you want to do, use the GNU Lesser General Public License instead of this +License. But first, please read +<>. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5c0896 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# OpsWorks CLI +[![CircleCI](https://circleci.com/gh/plivo/opsworks.svg?style=shield)](https://circleci.com/gh/plivo/opsworks) + +The missing OpsWorks CLI, run commands across stacks (and across regions), check your instances, apps, deployments, ELBs, with a smart filtering system. + +[![asciicast](https://asciinema.org/a/1fvxew0u6684jhvkj53ekkduc.png)](https://asciinema.org/a/1fvxew0u6684jhvkj53ekkduc) + +## Installation + +`npm install -g opsworks` + +## Usage + +``` +opsworks [args] +``` + +The OpsWorks CLI has multiple commands, similar to `git`, `apt-get` or `brew`. When you run `opsworks` with no arguments, you get an interactive prompt. + +### GUI / Prompt + +For simple tasks, just use `opsworks` without a command and you'll get an interactive prompt. + +![Prompt](https://raw.githubusercontent.com/plivo/opsworks/master/img/prompt.png) + +### Configuration + +`opsworks` needs to access the AWS API using your credentials. Just like the AWS SDK or CLI, it will look for credentials in two places : + +* From the shared credentials file (`~/.aws/credentials`) +* From environment variables + +To use the credentials file, create a `~/.aws/credentials` file based on the template below : + +``` +[default] +aws_access_key_id=your_access_key +aws_secret_access_key=your_secret_key +``` + +### Commands + +| command | description | +|-------------|-----------------------------| +| stacks | list OpsWorks stacks | +| deployments | list OpsWorks deployments | +| instances | list instances | +| apps | list apps | +| elbs | list Elastic Load Balancers | +| update | update cookbooks | +| setup | run setup recipes | +| configure | run configure recipes | +| deploy | deploy specified app | +| recipes | run specified recipes | + +#### Shared options for these commands + +* `-f` Specify filter (see below) +* `-u` Update cookbooks before running the command +* `-y` Do not ask for confirmation + +**Note:** by default, when you do not specify `-y`, the CLI will display a summary of what commands it will run and on which layer of which stacks as a precaution. + +#### Filtering + +Any `opsworks` command accepts filters. There are three built-in filters : + +| field | description | +|--------|--------------------------------| +| layer | The **Shortname** of the layer | +| stack | The **Name** of the stack | +| region | The stack's **region** | + +The format is `field:filter,field2:filter2,...` +You can use wildcards, or even use regexes. + +For example the command bellow would match all stacks whose name contain `wordpress`, and only include their **database** layer. + +``` +opsworks instances -f 'stack:*wordpress*,layer:database' +``` + +Using regexes to check ELBs of two wordpress stacks at once : + +``` +opsworks instances -f 'stack:(prod|staging)-wordpress' +``` + +Additionally, if you use [custom JSON](http://docs.aws.amazon.com/opsworks/latest/userguide/workingstacks-json.html) on your stacks or layers, you can use arbitrary filters. For example, if your custom JSON has an **env** variable, this would work : + +``` +opsworks instances -f 'env:production' +``` + +## Issues? + +Please feel free to [open an issue](https://github.com/plivo/opsworks/issues/new) if you find a bug or to request a feature. Please make sure to include all relevant logs. + +## Authors + +Developed by [Tristan Foureur](https://github.com/esya) for [Plivo](https://www.plivo.com) + +## License + +Copyright © Plivo Inc. + +All code is licensed under the GPL, v3 or later. See `LICENSE.md` file for details. diff --git a/bin/opsworks b/bin/opsworks new file mode 100755 index 0000000..e64ba42 --- /dev/null +++ b/bin/opsworks @@ -0,0 +1,107 @@ +#!/usr/bin/env node +var CommandRunner = require('./../lib/commandrunner'); +var Logger = require('./../lib/logger'); +var config = require('./../lib/config'); + +var argv = require('yargs') + .usage('$0 [args]') + .strict() + .command('stacks', 'list OpsWorks stacks', function (yargs) { + yargs.command = 'toooo'; + return yargs + .usage('$0 stacks\nWill list all the stacks matching your filters') + .option('f', { + description: 'Filters' + }) + }, function(argv) { argv.command = 'stacks' }) + .command('deployments', 'list OpsWorks deployments', function (yargs) { + yargs.command = 'toooo'; + return yargs + .usage('$0 deployments\nWill list all the deployments on stacks matching your filters') + .option('f', { + description: 'Filters' + }) + .boolean('i').alias('i', 'instances').describe('i','Show instances affected by deployment').default('i',false) + .option('n', { + description: 'Number of deployments to display per stack' + }) + .number('n') + .default('n',5); + }, function(argv) { argv.command = 'deployments' }) + .command('instances', 'list instances', function (yargs) { + return yargs + .usage('$0 instances\nWill list all instances per stack&layer matching your filters.') + .option('f', { + description: 'Filters' + }) + }, function(argv) { argv.command = 'instances' }) + .command('apps', 'list apps', function (yargs) { + return yargs + .usage('$0 apps\nWill list all apps per stack matching your filters.') + .option('f', { + description: 'Filters' + }) + }, function(argv) { argv.command = 'apps' }) + .command('elbs', 'list elastic load balancers', function (yargs) { + return yargs + .usage('$0 apps\nWill list all ELBs per stack/layers matching your filters.') + .option('f', { + description: 'Filters' + }) + }, function(argv) { argv.command = 'elbs' }) + .command('update', 'updates cookbooks', function (yargs) { + return yargs + .usage('$0 update\nWill update the cookbooks on the stacks and layers matching your filters, and wait for the update to be completed.') + .option('f', { + description: 'Filters' + }) + }, function(argv) { argv.command = 'update' }) + .command('setup', 'runs setup recipes', function (yargs) { + return yargs + .usage('$0 stacks\nRuns the setup deployment on the stacks and layers matching your filters, and wait for the setup to be completed.') + .option('f', { + description: 'Filters' + }) + .boolean('u').alias('u','update-first').describe('u','Update the cookbooks on the instances first') + .default('u',false) + }, function(argv) { argv.command = 'setup' }) + .command('configure', 'runs configure recipes', function (yargs) { + return yargs + .usage('$0 configure\nRuns the configure deployment on the stacks and layers matching your filters, and wait for the configuration to be completed.') + .option('f', { + description: 'Filters' + }) + .boolean('u').alias('u','update-first').describe('u','Update the cookbooks on the instances first') + .default('u',false) + }, function(argv) { argv.command = 'configure' }) + .command('deploy ', 'runs deploy recipes', function (yargs) { + return yargs + .usage('$0 deploy \nRuns the "deploy" deployment on the stacks and layers matching your filters, and wait for the deployment to be completed.') + .option('f', { + description: 'Filters' + }) + .boolean('u').alias('u','update-first').describe('u','Update the cookbooks on the instances first') + .default('u',false) + }, function(argv) { argv.command = 'deploy' }) + .command('recipes ', 'runs custom recipes', function (yargs) { + return yargs + .usage('$0 recipes') + .option('f', { + description: 'Filters' + }) + .boolean('u').alias('u','update-first').describe('u','Update the cookbooks on the instances first') + .default('u',false) + }, function(argv) { argv.command = 'recipes' }) + .help('h') + .boolean('v').alias('v', 'verbose').describe('v','Turns on verbose mode').default('v',false).global('v') + .boolean('y').alias('y', 'yes').describe('y','Automatic yes to prompts, to run non-interactively').default('y',false).global('y') + .example("deployments -f 'stack:production-*'",'List deployments for all production stacks') + .example("recipes common::setup_users",'Execute setup_users on all your stacks') + .example("setup -f layer:webapp,env:production",'Runs the setup deployment on "webapp" production layers') + .argv; + +var CR = new CommandRunner(argv); +CR.runCommand().catch(err => { + Logger.error(err); + Logger.error(`Full verbose log at ${config.logfile}`) +}); diff --git a/config/config.yml b/config/config.yml new file mode 100644 index 0000000..2064b4a --- /dev/null +++ b/config/config.yml @@ -0,0 +1,8 @@ +default: + # Interval at which to poll opsworks API to check deployments. + pollInterval: 10000 + verbosity: info + logfile: /tmp/opsworks.log + alias: + p-mediaserver: 'layer:mediaserver,env:production' + s-mediaserver: 'layer:mediaserver,env:staging' diff --git a/img/prompt.png b/img/prompt.png new file mode 100644 index 0000000000000000000000000000000000000000..eb60048088e819ddf63028c30d57b34c9b7eca02 GIT binary patch literal 26212 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 { + this.aws.describeStacks({}, function(err, data) { + if (err) reject(err, err.stack); // an error occurred + else resolve(data.Stacks); // successful response + }); + }); + } + + runCommands(stacks,type,args) { + if(!stacks.length) + throw new Error("No stacks matching your filters"); + + Logger.info(`Running command ${type} on ${stacks.length} stacks`); + + var p = []; + var StackById = {}; + + stacks.forEach(stack => { + StackById[stack.StackId] = stack; + p.push(this.runCommand(stack,type,args)) + }); + + return Promise.all(p) + .then(this.monitorDeployments.bind(this)) + .catch(function(e) { + if(e.length) { + var deployments = e; + var f = 0; + deployments.forEach(d => { + var stack = StackById[d.StackId]; + if(d.Status == 'failed') { + var logUrl = `https://console.aws.amazon.com/opsworks/home?region=${stack.Region}#/stack/${stack.StackId}/deployments/${d.DeploymentId}`; + + Logger.error(`Deployment failed on stack ${stack.Name}, logs: ${logUrl}`); + f++; + } + }); + + Logger.error(`Failed deployment on ${f}/${deployments.length} stacks`); + } else { + if(e.message.match(/at least an instance ID/)) { + throw new Error("No running instance match your filters"); + } else { + throw e; + } + } + }) + .then(function() { + Logger.info("Done"); + }); + } + + runCommand(stack,type,args) { + + var layers = []; + var layersNames = []; + stack.layers.forEach(layer => { + layers.push(layer.LayerId) + layersNames.push(layer.Shortname); + }); + + Logger.debug(`Running command ${type} on ${stack.Name}, layers : ${layersNames.join(',')}`); + + var params = { + Command: { + Name: type + }, + StackId: stack.StackId, + LayerIds: layers + } + + Logger.debug('Params:',params); + + if(type == 'execute_recipes') { + params.Command.Args = {recipes: args}; + } else if(type == 'deploy') { + stack.apps.forEach(app => { + if(app.Shortname == args) { + params.AppId = app.AppId; + } + }); + + if(!params.AppId) { + throw new Error(`Could not find app ${args} on stack ${stack.Name}`); + } + } + + return new Promise((resolve, reject) => { + this.aws.createDeployment(params, function(err, data) { + Logger.debug(err,data); + if (err) reject(err, err.stack); // an error occurred + else { + resolve(data.DeploymentId); + } + }); + }); + } + + monitorDeployments(ids) { + Logger.debug("Monitoring deployments of ",ids); + var interval = null; + return new Promise((resolve, reject) => { + Logger.info("Monitoring deployments, please be patient..."); + this.checkDeployments(ids,resolve,reject); + }) + } + + checkDeployments(ids,resolve,reject) { + Logger.debug("Checking deployments status"); + var self = this; + this.aws.describeDeployments({DeploymentIds: ids}, function(err, data) { + if (err) reject(err, err.stack); // an error occurred + else { + Logger.debug("Deployments : ",data); + var Deployments = data.Deployments; + + var counts = {}; + var total = Deployments.length; + + Deployments.forEach(d => { + if(!counts[d.Status]) counts[d.Status] = 0; + counts[d.Status]++; + }); + + if(counts['successful'] && counts['successful'] == total) { + resolve(Deployments); + } + else if(!counts['running']) + //If no deployment is running but not all are successful => error & all done. + reject(Deployments); + else + setTimeout(self.checkDeployments.bind(self,ids,resolve,reject),10000); + } + }); + } + + // Adds instances to each layer + fetchInstances(stacks) { + return Promise.all(stacks.map(this.fetchInstancesForStack.bind(this))); + } + + fetchInstancesForStack(stack) { + Logger.debug(`Fetching instances for ${stack.Name} - ${stack.StackId}`); + return new Promise((resolve, reject) => { + this.aws.describeInstances({StackId: stack.StackId}, function(err, data) { + Logger.debug(`Found ${data.Instances.length} instances for ${stack.Name} - ${stack.StackId}`) + if (err) reject(err, err.stack); // an error occurred + else resolve(data.Instances); // successful response + }); + }).then(function(Instances) { + Instances.forEach(i => { + stack.layers.forEach(layer => { + if(!layer.instances) layer.instances = []; + if(layer.LayerId == i.LayerIds[0]) { + layer.instances.push(i); + } + }); + }); + + return stack; + }); + } + + fetchDeployments(stacks) { + return Promise.all(stacks.map(this.fetchDeploymentsForStack.bind(this))); + } + + fetchDeploymentsForStack(stack) { + Logger.debug(`Fetching deployments for ${stack.Name} - ${stack.StackId}`); + return new Promise((resolve, reject) => { + this.aws.describeDeployments({StackId: stack.StackId}, function(err, data) { + if (err) reject(err, err.stack); // an error occurred + else resolve(data.Deployments); // successful response + }); + }).then(function(Deployments) { + if(!stack.deployments) + stack.deployments = []; + Deployments.forEach(deployment => { + stack.deployments.push(deployment) + }); + + return stack; + }); + } + + fetchApps(stacks) { + return Promise.all(stacks.map(this.fetchAppsForStack.bind(this))); + } + + fetchElbs(stacks) { + return Promise.all(stacks.map(this.fetchElbsForStack.bind(this))); + } + + fetchAppsForStack(stack) { + Logger.debug(`Fetching apps for ${stack.Name} - ${stack.StackId}`); + return new Promise((resolve, reject) => { + this.aws.describeApps({StackId: stack.StackId}, function(err, data) { + if (err) reject(err, err.stack); // an error occurred + else resolve(data.Apps); // successful response + }); + }).then(function(Apps) { + if(!stack.apps) + stack.apps = []; + Apps.forEach(app => { + stack.apps.push(app) + }); + + return stack; + }); + } + + fetchHealthForELB(ELB) { + return new Promise((resolve, reject) => { + var awsELB = new AWS.ELB({region: ELB.Region}); + awsELB.describeInstanceHealth({LoadBalancerName: ELB.ElasticLoadBalancerName},function(err,data) { + if (err) reject(err, err.stack); + else { + ELB.InstanceStates = data.InstanceStates; + resolve(ELB); + } + }); + }); + } + + fetchElbsForStack(stack) { + var self = this; + Logger.debug(`Fetching ELBs for ${stack.Name} - ${stack.StackId}`); + return new Promise((resolve, reject) => { + this.aws.describeElasticLoadBalancers({StackId: stack.StackId}, function(err, data) { + if (err) reject(err, err.stack); // an error occurred + else resolve(data.ElasticLoadBalancers); // successful response + }); + }).then(function(Elbs) { + return Promise.all(Elbs.map(self.fetchHealthForELB.bind(self))); + }).then(function(Elbs) { + if(!stack.elbs) + stack.elbs = []; + + Elbs.forEach(elb => { + stack.elbs.push(elb) + if(stack.layers) { + stack.layers.forEach(layer => { + if(!layer.elbs) + layer.elbs = []; + + if(elb.LayerId == layer.LayerId) + layer.elbs.push(elb); + }); + } + }); + return stack; + }); + } + + + fetchLayers(stack) { + //For arrays, find layers of all stacks + if(stack.length) { + return Promise.all(stack.map(this.fetchLayers.bind(this))); + } + + return new Promise((resolve, reject) => { + this.aws.describeLayers({StackId: stack.StackId}, function(err, data) { + if (err) reject(err, err.stack); // an error occurred + else { + stack.layers = data.Layers; + resolve(stack); + } + }); + }); + } + + /** + * Given a list of stacks with layers, instances and ELBs, + * Match the instance IDs of the ELB with the "friendly" names + * used in the opsworks stack. + * + * @param {array} stacks + * @return {array} stacks + */ + matchInstancesInElbs(stacks) { + stacks.forEach(stack => { + stack.layers.forEach(function(layer,index) { + if(!layer.elbs) { + delete stack.layers[index]; + return; + } + layer.elbs.forEach(elb => { + elb.instances = []; + elb.Ec2InstanceIds.forEach(id => { + layer.instances.forEach(instance => { + if(instance.Ec2InstanceId == id) + { + elb.InstanceStates.forEach(state => { + if(state.InstanceId == id) { + instance.state = state; + elb.instances.push(instance); + } + }); + } + }); + }) + }) + + }); + }); + return stacks; + } + + findStackByName(name) { + return this.listStacks().then(stacks => { + var foundStack = null; + stacks.forEach(stack => { + if(stack.Name == name) { + foundStack = stack; + return; + } + }); + + if(foundStack) + return foundStack; + else + throw new Error("Cannot find stack "+name); + }); + } +} + +module.exports = Opsworks; diff --git a/lib/StackList.js b/lib/StackList.js new file mode 100644 index 0000000..a21e502 --- /dev/null +++ b/lib/StackList.js @@ -0,0 +1,87 @@ +'use strict'; + +var Logger = require('./logger'); +class StackList { + constructor (stacks) { + this.stacks = stacks; + this.stacks.forEach((stack, i) => { + if(stack.CustomJson) + var stackJSON = JSON.parse(stack.CustomJson); + else + var stackJSON = {}; + + if(!stack.layers) stack.layers = []; + stack.layers.forEach((layer, j) => { + var layerJSON = (layer.CustomJson) ? JSON.parse(layer.CustomJson) : {}; + this.stacks[i].layers[j].custom_json = Object.assign({}, stackJSON,layerJSON); + this.stacks[i].layers[j].custom_json.id = {i:i,j:j}; + }); + }); + + } + + applyFilters (filters) { + Logger.debug(`Got ${this.stacks.length} stacks, applying filters`,filters); + var seen = {}; + filters.forEach(filter => { + var field = filter.split(':')[0]; + if(!seen[field]) + seen[field] = true; + else { + throw new Error("Cannot use the same filter twice."); + } + + this.applyFilter(filter) + }); + Logger.debug(`Got ${this.stacks.length} stacks after filters`,filters); + return true; + } + + applyFilter (filter) { + var split = filter.split(':'); + + if(split.length != 2) + throw new Error("Incorrect filter (Format: name:value)"); + + var field = split[0]; + var filter = split[1]; + var Regex = new RegExp('^'+filter.replace(/[^.]?\*/g,'.*')+'$'); + Logger.debug(`Filter ${filter} turned to regex ${Regex}`); + + //Stack-level filter by default + var layerFilter = false; + if(field == 'stack') + field = 'Name'; + else if(field == 'region') + field = 'Region'; + else { + layerFilter = true; + } + + var tmp = []; + this.stacks.forEach(stack => { + if(!layerFilter && stack[field] && stack[field].match(Regex)) + tmp.push(stack); + else if(layerFilter) { + var layers = []; + stack.layers.forEach(layer => { + if(field == 'layer' && layer.Shortname.match(Regex)) { + layers.push(layer); + } else if(layer.custom_json && layer.custom_json[field] && layer.custom_json[field].match(Regex)) { + // It's a custom JSON filter. + layers.push(layer) + } + }); + stack.layers = layers; + + //Push stack with only matching layers. + if(stack.layers.length > 0) + tmp.push(stack); + } + }); + + this.stacks = tmp; + } +} + +module.exports = StackList; diff --git a/lib/commandrunner.js b/lib/commandrunner.js new file mode 100644 index 0000000..ab6c25a --- /dev/null +++ b/lib/commandrunner.js @@ -0,0 +1,232 @@ +'use strict'; + +var config = require('./config'); +var Logger = require('./logger'); +var OpsWorks = require('./Opsworks'); +var StackList = require('./StackList'); +var Prompt = require('./prompt'); +var hierarchy = require('./hierarchy'); + +class CommandRunner { + constructor(argv) { + this.argv = argv; + this.hierarchy = new hierarchy(argv); + Logger.debug("Argv : ",this.argv); + this.OpsWorks = new OpsWorks(); + + if(argv.v) { + Logger.transports.console.level = 'debug'; + } + } + + /** + * Point of entry, uses the argv of this run to determine what to do + * + * If a command is specified, called the associated method + * Ex: `stacks` calls stacks() + * Otherwise, call runCli() to get the interactive cli + * + * @return {void} + */ + runCommand() { + Logger.debug("Running command",this.argv); + + if(this.argv._.length > 1) { + var err = new Error("Got more than one command : "+this.argv._.join(', ')); + Logger.error(err.message); + throw err; + } + + if(this.argv.command) { + this.filters = (this.argv.f) ? this.argv.f.split(',') : []; + return this[this.argv.command](); + } else { + this.filters = []; + return this.prompt(); + } + } + + deployments() { + var filters = this.filters; + Logger.debug("Getting the list of deployments"); + return this.OpsWorks.listStacks() + .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks)) + .then(function(stacks) { + var SL = new StackList(stacks); + filters.forEach(filter => { + if(filter.split(':')[0] === 'layer') { + Logger.warn("You specified a layer filter, deployments are per stack and not per layer.\nFetching deployments for stacks that match your filters."); + } + }); + SL.applyFilters(filters); + return SL.stacks; + }) + .then(this.OpsWorks.fetchDeployments.bind(this.OpsWorks)) + .then(function(stacks) { + stacks = stacks.map(stack => { stack.layers = []; return stack; }); + return stacks; + }) + .then(this.hierarchy.display.bind(this.hierarchy)) + } + + stacks() { + var filters = this.filters; + Logger.debug("Getting the list of stacks"); + return this.OpsWorks.listStacks() + .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks)) + .then(function(stacks) { + var SL = new StackList(stacks); + SL.applyFilters(filters); + return SL.stacks; + }) + .then(this.hierarchy.display.bind(this.hierarchy)) + } + + instances() { + var filters = this.filters; + Logger.debug("Getting the list of instances"); + return this.OpsWorks.listStacks() + .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks)) + .then(function(stacks) { + var SL = new StackList(stacks); + SL.applyFilters(filters); + return SL.stacks; + }) + .then(this.OpsWorks.fetchInstances.bind(this.OpsWorks)) + .then(this.hierarchy.display.bind(this.hierarchy)) + } + + elbs() { + var filters = this.filters; + Logger.debug("Getting the list of ELBs"); + return this.OpsWorks.listStacks() + .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks)) + .then(function(stacks) { + var SL = new StackList(stacks); + SL.applyFilters(filters); + return SL.stacks; + }) + .then(this.OpsWorks.fetchElbs.bind(this.OpsWorks)) + .then(this.OpsWorks.fetchInstances.bind(this.OpsWorks)) + .then(this.OpsWorks.matchInstancesInElbs.bind(this.OpsWorks)) + .then(this.hierarchy.display.bind(this.hierarchy)) + } + + apps() { + var filters = this.filters; + Logger.debug("Getting the list of stacks"); + return this.OpsWorks.listStacks() + .then(this.OpsWorks.fetchApps.bind(this.OpsWorks)) + .then(function(stacks) { + var SL = new StackList(stacks); + SL.applyFilters(filters); + return SL.stacks; + }) + .then(this.hierarchy.display.bind(this.hierarchy)) + } + + getFilteredStacks() { + var filters = this.filters; + Logger.debug("Getting the list of stacks"); + return this.OpsWorks.listStacks() + .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks)) + .then(function(stacks) { + var SL = new StackList(stacks); + SL.applyFilters(filters); + return SL.stacks; + }); + } + + // Commands : deploy,update,configure,setup + update() { + return this.getFilteredStacks() + .then(this.askConfirmation.bind(this)) + .then(stacks => { + return this.OpsWorks.runCommands(stacks,'update_custom_cookbooks'); + }); + } + + deploy() { + return this.getFilteredStacks() + .then(this.OpsWorks.fetchApps.bind(this.OpsWorks)) + .then(this.askConfirmation.bind(this)) + .then(stacks => { + if(this.argv.u) { + return this.OpsWorks.runCommands(stacks,'update_custom_cookbooks') + .then(function() { return stacks; }) + } else { + return stacks; + } + }) + .then(stacks => { + return this.OpsWorks.runCommands(stacks,'deploy',this.argv.application_name); + }); + } + + recipes() { + var recipes = this.argv.recipes_list.split(','); + return this.getFilteredStacks() + .then(this.OpsWorks.fetchApps.bind(this.OpsWorks)) + .then(this.askConfirmation.bind(this)) + .then(stacks => { + if(this.argv.u) { + return this.OpsWorks.runCommands(stacks,'update_custom_cookbooks') + .then(function() { return stacks; }) + } else { + return stacks; + } + }) + .then(stacks => { + return this.OpsWorks.runCommands(stacks,'execute_recipes',recipes); + }); + } + + configure() { + return this.getFilteredStacks() + .then(this.askConfirmation.bind(this)) + .then(stacks => { + if(this.argv.u) { + return this.OpsWorks.runCommands(stacks,'update_custom_cookbooks') + .then(function() { return stacks; }) + } else { + return stacks; + } + }) + .then(stacks => { + return this.OpsWorks.runCommands(stacks,'configure'); + }); + } + + setup() { + return this.getFilteredStacks() + .then(this.askConfirmation.bind(this)) + .then(stacks => { + if(this.argv.u) { + return this.OpsWorks.runCommands(stacks,'update_custom_cookbooks') + .then(function() { return stacks; }) + } else { + return stacks; + } + }) + .then(stacks => { + return this.OpsWorks.runCommands(stacks,'setup'); + }); + } + + prompt() { + var prompt = new Prompt(this); + return prompt.start(); + } + + askConfirmation(stacks) { + if(this.argv.y) { + Logger.debug("Skipping confirmation because -y was supplied"); + return stacks; + } else { + var prompt = new Prompt(this); + return prompt.askConfirmation(stacks,this.argv); + } + } +} + +module.exports = CommandRunner; diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..24f6e50 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,5 @@ +var yaml_config = require('node-yaml-config'); + +var config = yaml_config.load(__dirname + '/../config/config.yml'); + +module.exports = config; diff --git a/lib/hierarchy.js b/lib/hierarchy.js new file mode 100644 index 0000000..271668c --- /dev/null +++ b/lib/hierarchy.js @@ -0,0 +1,196 @@ +'use strict'; + +var colors = require('colors'); +var archy = require('archy'); + +class Hierarchy { + constructor(argv) { + this.argv = argv; + + this.statusColors = { + 'booting': 'blue', + 'connection_lost': 'red', + 'online': 'green', + 'pending': 'blue', + 'rebooting': 'blue', + 'requested': 'blue', + 'running_setup': 'blue', + 'setup_failed': 'red', + 'shutting_down': 'red', + 'start_failed': 'red', + 'stop_failed': 'red', + 'stopped': 'reset', + 'stopping': 'blue', + 'terminated': 'grey', + 'terminating': 'blue' + } + } + + + addInstanceToLayer(node,instance) { + var instanceLabel = ""; + + instanceLabel += `${instance.Hostname[this.statusColors[instance.Status]]}`; + + if(instance.Status !== 'online') { + instanceLabel += ` - ${instance.Status[this.statusColors[instance.Status]]}`; + } + + var type = instance.InstanceType || 'OnPremises' ; + instanceLabel += ` - ${type}`; + + if(instance.PublicIp) + instanceLabel += ` (${instance.PublicIp})` + else if(instance.PrivateIp) { + instanceLabel += ` (${instance.PrivateIp})` + } + + node.nodes.push({label: instanceLabel, nodes: []}); + } + + addInstanceToELB(node,instance) { + var instanceLabel = ""; + var details = false; + + if(instance.state.State === 'InService') { + instanceLabel += "●".green+` ${instance.Hostname.green}`; + } else { + instanceLabel += "●".red+` ${instance.Hostname} - OutOfService`.red; + details = true; + } + + var type = instance.InstanceType || 'OnPremises' ; + instanceLabel += ` - ${type}`; + + if(instance.PublicIp) + instanceLabel += ` (${instance.PublicIp})` + else if(instance.PrivateIp) { + instanceLabel += ` (${instance.PrivateIp})` + } + + if(details) { + instanceLabel += "\n"+`ReasonCode: ${instance.state.ReasonCode},`.red+"\n"+`Description: ${instance.state.Description}`.red; + } + + node.nodes.push({label: instanceLabel, nodes: []}); + } + + + /** + * Displays a Hierarchy of stacks->layers->instances + * + * If the layers aren't attached to the Stacks object, they're omitted + * If the instances aren't attached to the layers, they're omitted + + * @param {object} stacks Stacks object from Opsworks.js + * @return {void} + */ + display(stacks) { + var tree = {}; + tree.label = 'Stacks'; + tree.nodes = []; + stacks.forEach(stack => { + var stackLabel = `${stack.Name.bold.green.underline} - ${stack.Region}`; + var stackNode = {label: stackLabel, nodes: []}; + + if(!stack.apps) stack.apps = []; + stack.apps.forEach(app => { + var label = `${app.Shortname.green}`; + for(var key in app.AppSource) { + if(key == 'SshKey') continue; + label += `\n${key}: ${app.AppSource[key]}`; + } + + var appNode = {label: label, nodes: []}; + stackNode.nodes.push(appNode); + }); + + stack.layers.forEach(layer => { + + var layerNode = {label: layer.Shortname, nodes: []}; + stackNode.nodes.push(layerNode); + if(!layer.instances) layer.instances = []; + + if(this.argv.command == 'instances') + layer.instances.forEach(this.addInstanceToLayer.bind(this,layerNode)); + else if(this.argv.command == 'elbs') { + layer.elbs.forEach(elb => { + var elbLabel = `${elb.ElasticLoadBalancerName.magenta} - ${elb.Region}` + var elbNode = {label: elbLabel, nodes: []}; + layerNode.nodes.push(elbNode); + elb.instances.forEach(this.addInstanceToELB.bind(this,elbNode)); + }); + } + }); + + if(!stack.deployments) stack.deployments = []; + var deployments = stack.deployments.slice(0,this.argv.n); + deployments.forEach(deployment => { + // console.log(deployment); + var label = `${deployment.CreatedAt} - ${deployment.Command.Name.bold}`; + var logUrl = false; + + switch(deployment.Status) { + case 'running': + label = label.blue; + break; + + case 'failed': + label = label.red; + logUrl = `https://console.aws.amazon.com/opsworks/home?region=${stack.Region}#/stack/${stack.StackId}/deployments/${deployment.DeploymentId}` + break; + + case 'successful': + label = label.green; + break; + } + + if(logUrl) { + label += "\n"+"Logs: ".red.bold + logUrl; + } + + if(deployment.IamUserArn) + label += `\nAuthor: ${deployment.IamUserArn.italic}`; + else + label += "\nAuthor: "+"Automatic AWS Deployment".italic; + + label += `\nStatus: ${deployment.Status}`; + + if(deployment.Duration) { + label += `\nDuration: ${deployment.Duration}s` + } + + if(deployment.Command.Name == 'execute_recipes') { + var recipes = deployment.Command.Args.recipes.join(','); + label += `\nRecipes: ${recipes}` + } + + if(deployment.Comment) { + label += `\nComment: ${deployment.Comment.italic}` + } + + if(deployment.CustomJson) { + label += `\nJSON: ${deployment.CustomJson.italic}` + } + + var depNode = []; + + if(this.argv.i) { + for(var i = 1; i < 5; i++) { + if(Math.random() > 0.8) { + depNode.push({label: `mediaserver${i}`.red.bold+' (fail)', nodes: []}); + } else + depNode.push({label: `mediaserver${i}`.green+' (success)', nodes: []}); + } + } + stackNode.nodes.push({label:label, nodes: depNode}); + }); + + tree.nodes.push(stackNode); + }); + + console.log(archy(tree)); + } +} + +module.exports = Hierarchy; diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..46c908c --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,17 @@ +var winston = require('winston'); +var config = require('./config'); + +var logger = module.exports = new (winston.Logger)({ + transports: [ + new (winston.transports.Console)({ + colorize: 'all', + level: config.verbosity + }), + new (winston.transports.File)({ + filename: config.logfile, + level: 'debug', + json: false, + options: {flags: 'w'} + }) + ] +}); diff --git a/lib/prompt.js b/lib/prompt.js new file mode 100644 index 0000000..fe2f55d --- /dev/null +++ b/lib/prompt.js @@ -0,0 +1,201 @@ +'use strict'; + +var config = require('./config'); +var Logger = require('./logger'); +var OpsWorks = require('./Opsworks'); +var StackList = require('./StackList'); +var inquirer = require('inquirer'); + +var colors = require('colors'); + +class Prompt { + constructor(CommandRunner) { + this.CommandRunner = CommandRunner; + this.OpsWorks = new OpsWorks(); + } + + start() { + var self = this; + + return inquirer.prompt([ + { + type: 'list', + name: 'command', + message: 'What do you want to do?', + choices: [ + {name: 'List stacks', value: 'stacks'}, + {name: 'List instances', value: 'instances'}, + {name: 'Inspect ELBs', value: 'elbs'}, + {name: 'Update cookbooks', value: 'update_custom_cookbooks'}, + {name: 'Deploy code', value: 'deploy'}, + {name: 'Run Configure', value: 'configure'}, + {name: 'Run Setup', value: 'setup'} + ] + } + ]).then(function (answer) { + switch(answer.command) { + case 'stacks': + return self.CommandRunner.stacks(); + + case 'instances': + return self.pickStack(answer) + .then(answers => { + self.CommandRunner.argv.command = 'instances'; + self.CommandRunner.filters = [`stack:${answers.stack.Name}`]; + return self.CommandRunner.instances(); + }); + + case 'elbs': + return self.pickStack(answer) + .then(answers => { + self.CommandRunner.argv.command = 'elbs'; + self.CommandRunner.filters = [`stack:${answers.stack.Name}`]; + return self.CommandRunner.elbs(); + }); + + case 'deploy': + return self.pickStack(answer) + .then(self.pickApp.bind(self)) + .then(self.pickLayers.bind(self)) + .then(answers => { + return self.OpsWorks.runCommands([answers.stack],answers.command,answers.app); + }); + + default: + return self.pickStack(answer) + .then(self.pickLayers.bind(self)) + .then(answers => { + var filteredLayers = []; + var stack = answers.stack; + + stack.layers.forEach(layer => { + if(answers.layers.indexOf(layer.Shortname) >= 0) + filteredLayers.push(layer); + }); + + stack.layers = filteredLayers; + return self.askConfirmation([stack],{command: answers.command}) + .then(function(stacks) { + return self.OpsWorks.runCommands(stacks,answers.command); + }); + }); + } + }); + } + + askConfirmation(stacks,argv) { + var targets = []; + stacks.forEach(stack => { + var layers = []; + stack.layers.forEach(layer => { + layers.push(layer.Shortname); + }); + targets.push(`${stack.Name.green}:${layers.join(', ')}`); + }); + + var commands = []; + if(argv.u) { + commands.push("update") + } + + commands.push(argv.command); + + console.log(`/!\\ Running ${commands.join(', ').red} on stacks /!\\`.bold); + console.log(targets.join("\n"),"\n"); + + return inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: 'Confirm the command?' + } + ]).then(function(answers) { + if(!answers.confirm) + throw new Error("Command aborted"); + else + return stacks; + }); + } + + pickStack(answers) { + return this.OpsWorks.listStacks() + .then(stacks => { + var choices = []; + stacks.forEach(stack => choices.push({ + name: stack.Name, + value: stack + })); + + return inquirer.prompt([ + { + type: 'list', + name: 'stack', + message: 'On which stack?', + choices: choices + }]) + .then(answer => { + answers.stack = answer.stack; + return answers; + }); + }); + } + + pickApp(answers) { + return this.OpsWorks.fetchAppsForStack(answers.stack) + .then(this.OpsWorks.fetchLayers.bind(this.OpsWorks)) + .then(stack => { + answers.stack = stack; + + var choices = []; + stack.apps.forEach(app => choices.push({ + name: app.Name, + value: app.Shortname + })); + + if(choices.length == 0) + throw new Error(`The stack ${stack.Name} has no App attached to it.`); + + return inquirer.prompt([ + { + type: 'list', + name: 'app', + message: 'Deploy which app?', + choices: choices + }]) + .then(answer => { + answers.app = answer.app; + return answers; + }); + }); + } + + pickLayers(answers) { + return this.OpsWorks.fetchLayers(answers.stack) + .then(stack => { + var choices = []; + stack.layers.forEach(layer => choices.push({ + name: layer.Name, + value: layer.Shortname + })); + + return inquirer.prompt([{ + type: 'checkbox', + message: 'Select layers', + pageSize: 102, + name: 'layers', + choices: choices, + validate: function (answer) { + if (answer.length < 1) { + return 'You must choose at least one layer.'; + } + return true; + } + }]).then(function(answer) { + answers.layers = answer.layers; + return answers; + }); + }); + } +} + +module.exports = Prompt; diff --git a/package.json b/package.json new file mode 100644 index 0000000..5361f6a --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "opsworks", + "version": "0.1.0", + "description": "CLI goodness for opsworks", + "main": "index.js", + "bin": { + "opsworks": "./bin/opsworks" + }, + "dependencies": { + "archy": "^1.0.0", + "aws-sdk": "^2.3.15", + "bluebird": "^2.9.34", + "colors": "^1.1.2", + "inquirer": "^1.0.2", + "node-yaml-config": "0.0.3", + "winston": "^2.2.0", + "yargs": "^4.7.1" + }, + "devDependencies": { + "mocha": "^2.4.5", + "prettyjson": "^1.1.3", + "should": "^8.3.2", + "sinon": "^1.17.4", + "sinon-as-promised": "^4.0.0" + }, + "scripts": { + "test": "mocha -R spec -r should -r sinon" + }, + "author": "", + "license": "ISC" +} diff --git a/test/data/apps.json b/test/data/apps.json new file mode 100644 index 0000000..cb89183 --- /dev/null +++ b/test/data/apps.json @@ -0,0 +1,52 @@ +{ + "Apps": [ + { + "AppId": "123456789-abcdefgh-123456789", + "AppSource": { + "Revision": "master", + "SshKey": "*****FILTERED*****", + "Type": "git", + "Url": "git@github.com:repo/dummyapp1.git" + }, + "Attributes": { + "AutoBundleOnDeploy": "true", + "AwsFlowRubySettings": "{}", + "DocumentRoot": null, + "RailsEnv": null + }, + "CreatedAt": "2016-05-31T21:58:24+00:00", + "DataSources": [], + "Description": "DummyApp1", + "EnableSsl": false, + "Name": "Dummy App 1", + "Shortname": "dummyapp1", + "SslConfiguration": {}, + "StackId": "fakestackid-123456", + "Type": "other" + }, + { + "AppId": "123456789-abcdefgh-123456789", + "AppSource": { + "Revision": "master", + "SshKey": "*****FILTERED*****", + "Type": "git", + "Url": "git@github.com:repo/dummyapp2.git" + }, + "Attributes": { + "AutoBundleOnDeploy": "true", + "AwsFlowRubySettings": "{}", + "DocumentRoot": null, + "RailsEnv": null + }, + "CreatedAt": "2016-05-31T21:58:24+00:00", + "DataSources": [], + "Description": "DummyApp2", + "EnableSsl": false, + "Name": "Dummy App 2", + "Shortname": "dummyapp2", + "SslConfiguration": {}, + "StackId": "fakestackid-123456", + "Type": "other" + } + ] +} diff --git a/test/data/elbs.json b/test/data/elbs.json new file mode 100644 index 0000000..8a86ff3 --- /dev/null +++ b/test/data/elbs.json @@ -0,0 +1,26 @@ +{ + "ElasticLoadBalancers": [ + { + "StackId": "fake-stack-id-123456789", + "ElasticLoadBalancerName": "fake-loadbalancer-123456", + "VpcId": "vpc-6c260509", + "Ec2InstanceIds": [ + "i-instance1", + "i-instance2", + "i-instance3", + "i-instance4" + ], + "Region": "us-west-1", + "DnsName": "afakeelbdns.us-west-1.elb.amazonaws.com", + "LayerId": "123456789-random-id-obfuscated", + "AvailabilityZones": [ + "us-west-1a", + "us-west-1b" + ], + "SubnetIds": [ + "subnet-number1", + "subnet-number2" + ] + } + ] +} diff --git a/test/data/healthELB.json b/test/data/healthELB.json new file mode 100644 index 0000000..0f1a164 --- /dev/null +++ b/test/data/healthELB.json @@ -0,0 +1,28 @@ +{ + "InstanceStates": [ + { + "InstanceId": "i-instance1", + "ReasonCode": "N/A", + "State": "InService", + "Description": "N/A" + }, + { + "InstanceId": "i-instance2", + "ReasonCode": "N/A", + "State": "InService", + "Description": "N/A" + }, + { + "InstanceId": "i-instance3", + "ReasonCode": "N/A", + "State": "InService", + "Description": "N/A" + }, + { + "InstanceId": "i-instance4", + "ReasonCode": "N/A", + "State": "InService", + "Description": "N/A" + } + ] +} diff --git a/test/data/layers.json b/test/data/layers.json new file mode 100644 index 0000000..e46c1bc --- /dev/null +++ b/test/data/layers.json @@ -0,0 +1,144 @@ +{ + "Layers": [ + { + "StackId": "123456789-random-id", + "LifecycleEventConfiguration": { + "Shutdown": { + "DelayUntilElbConnectionsDrained": false, + "ExecutionTimeout": 120 + } + }, + "CustomRecipes": { + "Undeploy": [], + "Setup": [ + "common::default" + ], + "Configure": [], + "Shutdown": [ + "common::shutdown" + ], + "Deploy": [] + }, + "Packages": [], + "Name": "Database Layer", + "CustomSecurityGroupIds": [ + "sg-12345" + ], + "CustomJson": "{\n \"env\": \"production\",\n \"profile\": \"production\",\n \"role\": \"tata-vpn\",\n \"sensu\": {\n \"subscriptions\": [\n \"basic-server-checks\",\n \"basic-server-metrics\"\n ]\n }\n}", + "DefaultRecipes": { + "Undeploy": [], + "Setup": [], + "Configure": [], + "Shutdown": [], + "Deploy": [] + }, + "VolumeConfigurations": [], + "AutoAssignElasticIps": false, + "EnableAutoHealing": true, + "AutoAssignPublicIps": false, + "UseEbsOptimizedInstances": false, + "LayerId": "123456789-random-id-obfuscated", + "Attributes": { + "JvmVersion": null, + "RailsStack": null, + "EnableHaproxyStats": null, + "MysqlRootPasswordUbiquitous": null, + "NodejsVersion": null, + "HaproxyHealthCheckUrl": null, + "GangliaPassword": null, + "Jvm": null, + "HaproxyHealthCheckMethod": null, + "RubyVersion": null, + "HaproxyStatsPassword": null, + "MysqlRootPassword": null, + "JavaAppServer": null, + "MemcachedMemory": null, + "HaproxyStatsUrl": null, + "BundlerVersion": null, + "PassengerVersion": null, + "ManageBundler": null, + "JavaAppServerVersion": null, + "HaproxyStatsUser": null, + "GangliaUser": null, + "JvmOptions": null, + "EcsClusterArn": null, + "RubygemsVersion": null, + "GangliaUrl": null + }, + "Shortname": "database", + "DefaultSecurityGroupNames": [], + "Type": "custom", + "CreatedAt": "2016-03-29T04:18:04+00:00" + }, + { + "StackId": "123456789-random-id", + "LifecycleEventConfiguration": { + "Shutdown": { + "DelayUntilElbConnectionsDrained": false, + "ExecutionTimeout": 120 + } + }, + "CustomRecipes": { + "Undeploy": [], + "Setup": [ + "common::default" + ], + "Configure": [], + "Shutdown": [ + "common::shutdown" + ], + "Deploy": [] + }, + "Packages": [], + "Name": "WebApp Layer", + "CustomSecurityGroupIds": [ + "sg-12345" + ], + "CustomJson": "{\n \"env\": \"production\",\n \"profile\": \"production\",\n \"role\": \"tata-vpn\",\n \"sensu\": {\n \"subscriptions\": [\n \"basic-server-checks\",\n \"basic-server-metrics\"\n ]\n }\n}", + "DefaultRecipes": { + "Undeploy": [], + "Setup": [], + "Configure": [], + "Shutdown": [], + "Deploy": [] + }, + "VolumeConfigurations": [], + "AutoAssignElasticIps": false, + "EnableAutoHealing": true, + "AutoAssignPublicIps": false, + "UseEbsOptimizedInstances": false, + "LayerId": "123456789-random-id-obfuscated-2", + "Attributes": { + "JvmVersion": null, + "RailsStack": null, + "EnableHaproxyStats": null, + "MysqlRootPasswordUbiquitous": null, + "NodejsVersion": null, + "HaproxyHealthCheckUrl": null, + "GangliaPassword": null, + "Jvm": null, + "HaproxyHealthCheckMethod": null, + "RubyVersion": null, + "HaproxyStatsPassword": null, + "MysqlRootPassword": null, + "JavaAppServer": null, + "MemcachedMemory": null, + "HaproxyStatsUrl": null, + "BundlerVersion": null, + "PassengerVersion": null, + "ManageBundler": null, + "JavaAppServerVersion": null, + "HaproxyStatsUser": null, + "GangliaUser": null, + "JvmOptions": null, + "EcsClusterArn": null, + "RubygemsVersion": null, + "GangliaUrl": null + }, + "Shortname": "webapp", + "DefaultSecurityGroupNames": [], + "Type": "custom", + "CreatedAt": "2016-03-29T04:18:04+00:00" + } + ] +} diff --git a/test/data/stacks.json b/test/data/stacks.json new file mode 100644 index 0000000..866d677 --- /dev/null +++ b/test/data/stacks.json @@ -0,0 +1,104 @@ +{ + "Stacks": [ + { + "StackId": "fake-stack-id-123456789", + "ServiceRoleArn": "arn:aws:iam::123456789:role/aws-opsworks-service-role", + "VpcId": "vpc-k381wugh", + "DefaultRootDeviceType": "ebs", + "Name": "wordpress-production", + "HostnameTheme": "Layer_Dependent", + "UseCustomCookbooks": true, + "UseOpsworksSecurityGroups": false, + "Region": "us-west-1", + "DefaultAvailabilityZone": "us-west-1b", + "CreatedAt": "2016-02-19T22:41:22+00:00", + "CustomJson": "{\"env\":\"test\",\"stackjsontest\":\"somevalue\"}", + "CustomCookbooksSource": { + "Url": "****FILTERED****", + "Username": "****FILTERED****", + "Password": "*****FILTERED*****", + "Type": "s3" + }, + "ConfigurationManager": { + "Version": "12", + "Name": "Chef" + }, + "ChefConfiguration": {}, + "DefaultSubnetId": "subnet-12345678", + "DefaultSshKeyName": "production-key", + "DefaultInstanceProfileArn": "arn:aws:iam::123456789:instance-profile/aws-opsworks-ec2-role", + "Attributes": { + "Color": "rgb(45, 114, 184)" + }, + "DefaultOs": "Custom", + "Arn": "arn:aws:opsworks:us-west-1:123456789:stack/fake-stack-id-123456789/", + "AgentVersion": "4006-20160121174723" + }, + { + "StackId": "fake-stack-id-123456789", + "ServiceRoleArn": "arn:aws:iam::123456789:role/aws-opsworks-service-role", + "VpcId": "vpc-k381wugh", + "DefaultRootDeviceType": "ebs", + "Name": "wordpress-staging", + "HostnameTheme": "Layer_Dependent", + "UseCustomCookbooks": true, + "UseOpsworksSecurityGroups": false, + "Region": "us-west-1", + "DefaultAvailabilityZone": "us-west-1b", + "CreatedAt": "2016-02-19T22:41:22+00:00", + "CustomCookbooksSource": { + "Url": "****FILTERED****", + "Username": "****FILTERED****", + "Password": "*****FILTERED*****", + "Type": "s3" + }, + "ConfigurationManager": { + "Version": "12", + "Name": "Chef" + }, + "ChefConfiguration": {}, + "DefaultSubnetId": "subnet-12345678", + "DefaultSshKeyName": "production-key", + "DefaultInstanceProfileArn": "arn:aws:iam::123456789:instance-profile/aws-opsworks-ec2-role", + "Attributes": { + "Color": "rgb(45, 114, 184)" + }, + "DefaultOs": "Custom", + "Arn": "arn:aws:opsworks:us-west-1:123456789:stack/fake-stack-id-123456789/", + "AgentVersion": "4006-20160121174723" + }, + { + "StackId": "fake-stack-id-123456789", + "ServiceRoleArn": "arn:aws:iam::123456789:role/aws-opsworks-service-role", + "VpcId": "vpc-k381wugh", + "DefaultRootDeviceType": "ebs", + "Name": "wordpress-dev", + "HostnameTheme": "Layer_Dependent", + "UseCustomCookbooks": true, + "UseOpsworksSecurityGroups": false, + "Region": "us-east-1", + "DefaultAvailabilityZone": "us-east-1b", + "CreatedAt": "2016-02-19T22:41:22+00:00", + "CustomCookbooksSource": { + "Url": "****FILTERED****", + "Username": "****FILTERED****", + "Password": "*****FILTERED*****", + "Type": "s3" + }, + "ConfigurationManager": { + "Version": "12", + "Name": "Chef" + }, + "ChefConfiguration": {}, + "DefaultSubnetId": "subnet-12345678", + "DefaultSshKeyName": "production-key", + "DefaultInstanceProfileArn": "arn:aws:iam::123456789:instance-profile/aws-opsworks-ec2-role", + "Attributes": { + "Color": "rgb(45, 114, 184)" + }, + "DefaultOs": "Custom", + "Arn": "arn:aws:opsworks:us-west-1:123456789:stack/fake-stack-id-123456789/", + "AgentVersion": "4006-20160121174723" + } + ] +} diff --git a/test/filters.js b/test/filters.js new file mode 100644 index 0000000..4c438b3 --- /dev/null +++ b/test/filters.js @@ -0,0 +1,131 @@ +'use strict'; + +var fs = require('fs'); +var StackList = require('./../lib/StackList'); +var SL; + +describe("Filters",function() { + + beforeEach(function() { + var stacks = []; + var stacksJSON = JSON.parse(fs.readFileSync("test/data/stacks.json").toString()); + stacksJSON.Stacks.forEach(stack => { + var layersJSON = JSON.parse(fs.readFileSync("test/data/layers.json").toString()); + stack.layers = layersJSON.Layers; + stacks.push(stack); + }); + SL = new StackList(stacks); + }); + + + + describe("Built-in",function() { + it("Based on region", function() { + SL.applyFilters(['region:us-west-1']); + SL.stacks.should.have.length(2); + }); + + it("Based on stack name", function() { + SL.applyFilters(['stack:wordpress-production']); + SL.stacks.should.have.length(1); + }); + + it("Based on layer name", function() { + SL.applyFilters(['layer:database']); + SL.stacks.should.have.length(3); + SL.stacks.forEach(stack => { + stack.layers.should.have.length(1); + }); + }); + }); + + describe("From custom JSON",function() { + it("Custom filter test", function() { + SL.applyFilters(['env:production']); + SL.stacks.should.have.length(3); + }); + + it("Layer JSON should override Stack JSON", function() { + //A stack has `env` set to test, but the layers override it. + SL.applyFilters(['env:test']); + SL.stacks.should.have.length(0); + }); + + it("Stack JSON should be queriable too", function() { + //The final json of a layer should be the result of + //merging the stack's and the layer's + SL.applyFilters(['stackjsontest:somevalue']); + SL.stacks.should.have.length(1); + }); + + it("Should not throw if the filter does not exist", function() { + SL.applyFilters.bind(['thisdoesnotexist:somevalue']); + }); + + it("Should not throw if a layer has no custom_json", function() { + var stacks = []; + var stacksJSON = JSON.parse(fs.readFileSync("test/data/stacks.json").toString()); + stacksJSON.Stacks.forEach(stack => { + var layersJSON = JSON.parse(fs.readFileSync("test/data/layers.json").toString()); + stack.layers = layersJSON.Layers; + stacks.push(stack); + }); + + delete(stacks[0].layers[0].CustomJson); + + SL = new StackList(stacks); + SL.applyFilters(['env:test']); + + SL.stacks.should.have.length(1); + SL.stacks[0].layers.should.have.length(1); + }); + }); + + describe("Filtering",function() { + it("Should throw using poorly formated filter", function() { + SL.applyFilters.bind(SL,['thisisnotafilter']).should.throw(/Incorrect filter/); + SL.applyFilters.bind(SL,['that:is:notright']).should.throw(/Incorrect filter/); + }); + + it("Should throw using the same filter twice", function() { + SL.applyFilters.bind(SL,['env:production','env:staging']).should.throw(/same filter/); + }); + + it("Should omit stacks with no layers", function() { + SL.applyFilters(['layer:thisdoesnotexist']); + SL.stacks.should.have.length(0); + }); + + it("Combined filters", function() { + SL.applyFilters(['region:us-west*','stack:wordpress*','layer:database']); + SL.stacks.should.have.length(2); + SL.stacks.forEach(stack => { + stack.layers.should.have.length(1); + }) + }); + + it("Wildcard before", function() { + SL.applyFilters(['stack:*']); + SL.stacks.should.have.length(3); + + SL.applyFilters(['stack:*-production']); + SL.stacks.should.have.length(1); + }); + + it("Wildcard after", function() { + SL.applyFilters(['stack:wordp*']); + SL.stacks.should.have.length(3); + + SL.applyFilters(['region:us-west*']); + SL.stacks.should.have.length(2); + }); + + it("Multiple wildcards", function() { + SL.applyFilters(['stack:*-*']); + SL.stacks.should.have.length(3); + + SL.applyFilters(['region:us-*-1']); + SL.stacks.should.have.length(3); + }); + }); +}); diff --git a/test/opsworks.js b/test/opsworks.js new file mode 100644 index 0000000..245f128 --- /dev/null +++ b/test/opsworks.js @@ -0,0 +1,268 @@ +'use strict'; + +var config = require('./../lib/config'); +var Logger = require('./../lib/logger'); + +Logger.transports.console.level = 'error'; + +var fs = require('fs'); +var sinon = require('sinon'); +require('sinon-as-promised') + +var OpsWorks = require('./../lib/Opsworks'); + + +describe("OpsWorks", function() { + var cli; + beforeEach(function() { + cli = new OpsWorks(); + + var stacksJSON = JSON.parse(fs.readFileSync("test/data/stacks.json").toString()); + var layersJSON = JSON.parse(fs.readFileSync("test/data/layers.json").toString()); + + var stub = sinon.stub(cli,'listStacks'); + stub.resolves(stacksJSON.Stacks); + + var stub2 = sinon.stub(cli.aws,'describeLayers', function(params,callback){ + callback(null,JSON.parse(fs.readFileSync("test/data/layers.json").toString())); + }); + }); + + describe("#findStackByName", function() { + it("Should find stack by name", function() { + return cli.findStackByName('wordpress-production').should.be.fulfilled() + .then(stack => { + stack.Name.should.be.equal('wordpress-production'); + }); + }); + + it("Should throw when the stack is not found", function() { + return cli.findStackByName('wordpress-notexisting').should.be.rejectedWith(/Cannot find/); + }); + }); + + describe("#fetchLayers", function() { + it("Should add the layers to a stack object", function() { + return cli.findStackByName('wordpress-production') + .then(cli.fetchLayers.bind(cli)) + .then(stack => { + stack.layers.should.have.length(2); + }); + }); + + it("Should add the layers to multiple stacks", function() { + return cli.listStacks() + .then(cli.fetchLayers.bind(cli)) + .then(stacks => { + stacks.should.have.length(3); + for (var i = stacks.length - 1; i >= 0; i--) { + stacks[i].layers.should.have.length(2); + } + }); + }); + }); + + describe("#fetchInstances", function() { + beforeEach(function() { + var stub2 = sinon.stub(cli.aws,'describeInstances'); + stub2.callsArgWith(1,null,{Instances: [{ + InstanceId: Math.random(), + LayerIds: ['123456789-random-id-obfuscated'] + }]}); + }); + + it("Should attach the instances to the layers", function() { + return cli.listStacks() + .then(cli.fetchLayers.bind(cli)) + .then(cli.fetchInstances.bind(cli)) + .then(stacks => { + stacks[0].layers.should.have.length(2); + stacks[0].layers[0].instances.should.have.length(1); + stacks[0].layers[1].instances.should.have.length(0); + }); + }); + + it("Should not attach instances with no matching layers", function() { + return cli.listStacks() + .then(cli.fetchLayers.bind(cli)) + .then(function(stacks) { + stacks[0].layers = stacks[0].layers.slice(0,1); + return stacks; + }) + .then(cli.fetchInstances.bind(cli)) + .then(stacks => { + stacks[0].layers.should.have.length(1); + stacks[0].layers[0].instances.should.have.length(1); + }); + }); + }); + + describe("#fetchApps", function() { + beforeEach(function() { + var stubApps = sinon.stub(cli.aws,'describeApps', function(params,callback){ + callback(null,JSON.parse(fs.readFileSync("test/data/apps.json").toString())); + }); + }); + + it("Should attach the apps to the stack", function() { + return cli.listStacks() + .then(cli.fetchApps.bind(cli)) + .then(stacks => { + stacks[0].apps.should.have.length(2); + stacks[1].apps.should.have.length(2); + stacks[0].apps[0].Shortname.should.be.equal('dummyapp1'); + stacks[0].apps[1].Shortname.should.be.equal('dummyapp2'); + }); + }); + }); + + describe("#fetchElbs", function() { + beforeEach(function() { + var stubElbs = sinon.stub(cli.aws,'describeElasticLoadBalancers', function(params,callback){ + callback(null,JSON.parse(fs.readFileSync("test/data/elbs.json").toString())); + }); + + var stub2 = sinon.stub(cli.aws,'describeInstances'); + stub2.callsArgWith(1,null,{Instances: [ + { + InstanceId: 'i-instance1', + Ec2InstanceId: 'i-instance1', + Name: 'instance1', + LayerIds: ['123456789-random-id-obfuscated'] + }, + { + InstanceId: 'i-instance2', + Ec2InstanceId: 'i-instance2', + Name: 'instance2', + LayerIds: ['123456789-random-id-obfuscated'] + }, + { + InstanceId: 'i-instance3', + Ec2InstanceId: 'i-instance3', + Name: 'instance3', + LayerIds: ['123456789-random-id-obfuscated'] + }, + { + InstanceId: 'i-instance4', + Ec2InstanceId: 'i-instance4', + Name: 'instance4', + LayerIds: ['123456789-random-id-obfuscated'] + } + ]}); + + cli.fetchHealthForELB = function(ELB) { + // var elbs = JSON.parse(fs.readFileSync("test/data/elbs.json").toString()); + // var elb = elbs.ElasticLoadBalancers[0]; + var health = JSON.parse(fs.readFileSync("test/data/healthELB.json").toString()); + ELB.InstanceStates = health.InstanceStates; + return ELB; + } + // var stub3 = sinon.stub(cli,'fetchHealthForELB'); + // stub3.resolves({}); + }); + + it("Should attach the Elbs to the corresponding layer", function() { + return cli.listStacks() + .then(cli.fetchLayers.bind(cli)) + .then(cli.fetchElbs.bind(cli)) + .then(stacks => { + stacks[0].layers[0].elbs.should.have.length(1); + stacks[0].layers[1].elbs.should.have.length(0); + }); + }); + + it("Should match instances to their ELBs", function() { + return cli.listStacks() + .then(cli.fetchLayers.bind(cli)) + .then(cli.fetchInstances.bind(cli)) + .then(cli.fetchElbs.bind(cli)) + .then(cli.matchInstancesInElbs.bind(cli)) + .then(stacks => { + stacks[0].layers[0].elbs.should.have.length(1); + stacks[0].layers[1].elbs.should.have.length(0); + stacks[0].layers[0].elbs[0].instances.should.have.length(4); + }); + }); + }); + + describe("#fetchDeployments", function() { + beforeEach(function() { + var stubDeployment = sinon.stub(cli.aws,'describeDeployments'); + stubDeployment.callsArgWith(1,null,{Deployments: [{ + Status: 'running' + },{ + Status: 'failed' + }]}); + + + }); + + it("Should attach the deployments to the stack", function() { + return cli.listStacks() + .then(cli.fetchDeployments.bind(cli)) + .then(stacks => { + stacks[0].deployments.should.have.length(2); + stacks[1].deployments.should.have.length(2); + }); + }); + }); + + describe("#runCommands / #monitorDeployment", function() { + var clock; + var stub2; + + beforeEach(function() { + var stub = sinon.stub(cli.aws,'createDeployment'); + stub.onCall(0).callsArgWith(1,null,{DeploymentId: 'fake-deployment-id-0'}); + stub.onCall(1).callsArgWith(1,null,{DeploymentId: 'fake-deployment-id-1'}); + stub.onCall(2).callsArgWith(1,null,{DeploymentId: 'fake-deployment-id-2'}); + + var count = 0; + stub2 = sinon.stub(cli.aws,'describeDeployments',(params,callback) => { + var status = (count++ == 5) ? 'successful' : 'running'; + var res = []; + + params.DeploymentIds.forEach((id,i) => { + if(i == 1) + status = 'successful'; + res.push({DeploymentId: id, Status: status}) + }); + + callback(null,{Deployments: res}); + + clock.tick(10000); + }); + + clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + clock.restore(); + }) + + it("Should check the deployment's status at regular intervals", function() { + return cli.findStackByName('wordpress-production') + .then(cli.fetchLayers.bind(cli)) + .then(stack => { + return cli.runCommand(stack,'fake_command'); + }) + .then(id => { + var p = cli.monitorDeployments([id]); + return p; + }); + }); + + it("Should be able to run commands across multiple stacks", function() { + return cli.listStacks() + .then(cli.fetchLayers.bind(cli)) + .then(stacks => { + var p = cli.runCommands(stacks,'fake_command'); + return p; + }) + .then(function() { + //describeDeployments should have been called 6 times + stub2.callCount.should.equal(6); + }); + }); + }); +});