[
  {
    "path": ".gitignore",
    "content": "coverage.out\n.idea/\n"
  },
  {
    "path": ".travis.yml",
    "content": "# sudo is required for docker\nsudo: required\n\nlanguage: go\n\ngo:\n - \"1.20.x\"\n\nenv:\n    - DEPLOY_DOCS=\"$(if [[ $TRAVIS_BRANCH == 'master' && $TRAVIS_PULL_REQUEST == 'false' ]]; then echo -n 'true' ; else echo -n 'false' ; fi)\"\n\nservices:\n    - docker\n\ninstall:\n    - docker pull mysql:8.0\n    - go install golang.org/x/lint/golint@latest\n    - go install github.com/kisielk/errcheck@latest\n    - go get github.com/spf13/afero@v0.0.0-20160919210114-52e4a6cfac46\n    - go get github.com/fsnotify/fsnotify@v1.3.2-0.20160816051541-f12c6236fe7b\n\n# With the \"docker\" tag enabled on go test invocation (-tags docker)\n# the mysql:5.6 docker container will be started\n# and the mysql tests will connect to this container\n# This requires us to stop the pre-installed mysql server\nscript:\n    - sudo service mysql stop\n    - diff -u <(echo -n) <(gofmt -d `find . -name '*.go' | grep -Ev '/vendor/|/migration'`)\n    - go list ./... | grep -Ev '/vendor/|/migration' | xargs -L1 golint\n    - go vet `go list ./... | grep -v /vendor/`\n    - errcheck -ignore 'io:Close' -ignoretests `go list ./... | grep -v /vendor/`\n    - go test -v  ./...\n\nafter_success:\n    - ./update-docs.sh\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2015 Netflix, Inc.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: check fmt lint errcheck test build\n\nSHELL:=/bin/bash\n\nbuild: check\n\tgo build github.com/Netflix/chaosmonkey/cmd/chaosmonkey\n\ncheck: fmt lint errcheck\n\ngofmt: fmt\n\nfmt: \n\tdiff -u <(echo -n) <(gofmt -d `find . -name '*.go' | grep -Ev '/vendor/|/migration'`)\n\nlint:\n\tgo list ./... | grep -Ev '/vendor/|/migration' | xargs -L1 golint\n\nerrcheck:\n\terrcheck -ignore 'io:Close' -ignoretests `go list ./... | grep -v /vendor/`\n\ntest:\n\tgo test -v  ./...\n\n\n# Coverage testing\ncover:\n\techo 'mode: atomic' > coverage.out \n\tgo list ./... | grep -Ev '/vendor/|/migration' | xargs -n1 -I{} sh -c 'go test -covermode=atomic -coverprofile=coverage.tmp {} && tail -n +2 coverage.tmp >> coverage.out' && rm coverage.tmp\n\tgo tool cover -html=coverage.out\n\nfix:\n\tgofmt -w -s `find . -name '*.go' | grep -Ev '/vendor/|/migration'`\n"
  },
  {
    "path": "NOTICE",
    "content": "Chaos Monkey randomly terminates instances.\nCopyright (C) 2016 Netflix, Inc.\n\nChaos Monkey makes use of several third-party OSS libraries that are included in\nthe vendor directory.\n\n\n# go-spew\n\nGo-spew is used for pretty-printing some Go data structures for debugging.\n\nhttp://github.com/davecgh/go-spew\n\n\nCopyright (c) 2012-2013 Dave Collins <dave@davec.name>\n\nPermission to use, copy, modify, and distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n\n# fsnotify\n\nFile system notifications for Go\n\n\nhttp://github.com/fsnotify/fsnotify\n\n\nCopyright (c) 2012 The Go Authors. All rights reserved.\nCopyright (c) 2012 fsnotify Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n\n# Go MySQL Driver\n\nGo MySQL Driver implements a MySQL driver.\n\nhttp://github.com/go-sql-driver/mysql\n\n\nMozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n\n\n# HCL\n\nHCL (HashiCorp Configuration Language) is a configuration language built by HashiCorp.\n\nhttp://github.com/hashicorp/hcl\n\n\nMozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. “Contributor”\n\n     means each individual or legal entity that creates, contributes to the\n     creation of, or owns Covered Software.\n\n1.2. “Contributor Version”\n\n     means the combination of the Contributions of others (if any) used by a\n     Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution”\n\n     means Covered Software of a particular Contributor.\n\n1.4. “Covered Software”\n\n     means Source Code Form to which the initial Contributor has attached the\n     notice in Exhibit A, the Executable Form of such Source Code Form, and\n     Modifications of such Source Code Form, in each case including portions\n     thereof.\n\n1.5. “Incompatible With Secondary Licenses”\n     means\n\n     a. that the initial Contributor has attached the notice described in\n        Exhibit B to the Covered Software; or\n\n     b. that the Covered Software was made available under the terms of version\n        1.1 or earlier of the License, but not also under the terms of a\n        Secondary License.\n\n1.6. “Executable Form”\n\n     means any form of the work other than Source Code Form.\n\n1.7. “Larger Work”\n\n     means a work that combines Covered Software with other material, in a separate\n     file or files, that is not Covered Software.\n\n1.8. “License”\n\n     means this document.\n\n1.9. “Licensable”\n\n     means having the right to grant, to the maximum extent possible, whether at the\n     time of the initial grant or subsequently, any and all of the rights conveyed by\n     this License.\n\n1.10. “Modifications”\n\n     means any of the following:\n\n     a. any file in Source Code Form that results from an addition to, deletion\n        from, or modification of the contents of Covered Software; or\n\n     b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor\n\n      means any patent claim(s), including without limitation, method, process,\n      and apparatus claims, in any patent Licensable by such Contributor that\n      would be infringed, but for the grant of the License, by the making,\n      using, selling, offering for sale, having made, import, or transfer of\n      either its Contributions or its Contributor Version.\n\n1.12. “Secondary License”\n\n      means either the GNU General Public License, Version 2.0, the GNU Lesser\n      General Public License, Version 2.1, the GNU Affero General Public\n      License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form”\n\n      means the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”)\n\n      means an individual or a legal entity exercising rights under this\n      License. For legal entities, “You” includes any entity that controls, is\n      controlled by, or is under common control with You. For purposes of this\n      definition, “control” means (a) the power, direct or indirect, to cause\n      the direction or management of such entity, whether by contract or\n      otherwise, or (b) ownership of more than fifty percent (50%) of the\n      outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n     Each Contributor hereby grants You a world-wide, royalty-free,\n     non-exclusive license:\n\n     a. under intellectual property rights (other than patent or trademark)\n        Licensable by such Contributor to use, reproduce, make available,\n        modify, display, perform, distribute, and otherwise exploit its\n        Contributions, either on an unmodified basis, with Modifications, or as\n        part of a Larger Work; and\n\n     b. under Patent Claims of such Contributor to make, use, sell, offer for\n        sale, have made, import, and otherwise transfer either its Contributions\n        or its Contributor Version.\n\n2.2. Effective Date\n\n     The licenses granted in Section 2.1 with respect to any Contribution become\n     effective for each Contribution on the date the Contributor first distributes\n     such Contribution.\n\n2.3. Limitations on Grant Scope\n\n     The licenses granted in this Section 2 are the only rights granted under this\n     License. No additional rights or licenses will be implied from the distribution\n     or licensing of Covered Software under this License. Notwithstanding Section\n     2.1(b) above, no patent license is granted by a Contributor:\n\n     a. for any code that a Contributor has removed from Covered Software; or\n\n     b. for infringements caused by: (i) Your and any other third party’s\n        modifications of Covered Software, or (ii) the combination of its\n        Contributions with other software (except as part of its Contributor\n        Version); or\n\n     c. under Patent Claims infringed by Covered Software in the absence of its\n        Contributions.\n\n     This License does not grant any rights in the trademarks, service marks, or\n     logos of any Contributor (except as may be necessary to comply with the\n     notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n     No Contributor makes additional grants as a result of Your choice to\n     distribute the Covered Software under a subsequent version of this License\n     (see Section 10.2) or under the terms of a Secondary License (if permitted\n     under the terms of Section 3.3).\n\n2.5. Representation\n\n     Each Contributor represents that the Contributor believes its Contributions\n     are its original creation(s) or it has sufficient rights to grant the\n     rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n     This License is not intended to limit any rights You have under applicable\n     copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions\n\n     Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n     Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n     All distribution of Covered Software in Source Code Form, including any\n     Modifications that You create or to which You contribute, must be under the\n     terms of this License. You must inform recipients that the Source Code Form\n     of the Covered Software is governed by the terms of this License, and how\n     they can obtain a copy of this License. You may not attempt to alter or\n     restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form\n\n     If You distribute Covered Software in Executable Form then:\n\n     a. such Covered Software must also be made available in Source Code Form,\n        as described in Section 3.1, and You must inform recipients of the\n        Executable Form how they can obtain a copy of such Source Code Form by\n        reasonable means in a timely manner, at a charge no more than the cost\n        of distribution to the recipient; and\n\n     b. You may distribute such Executable Form under the terms of this License,\n        or sublicense it under different terms, provided that the license for\n        the Executable Form does not attempt to limit or alter the recipients’\n        rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n     You may create and distribute a Larger Work under terms of Your choice,\n     provided that You also comply with the requirements of this License for the\n     Covered Software. If the Larger Work is a combination of Covered Software\n     with a work governed by one or more Secondary Licenses, and the Covered\n     Software is not Incompatible With Secondary Licenses, this License permits\n     You to additionally distribute such Covered Software under the terms of\n     such Secondary License(s), so that the recipient of the Larger Work may, at\n     their option, further distribute the Covered Software under the terms of\n     either this License or such Secondary License(s).\n\n3.4. Notices\n\n     You may not remove or alter the substance of any license notices (including\n     copyright notices, patent notices, disclaimers of warranty, or limitations\n     of liability) contained within the Source Code Form of the Covered\n     Software, except that You may alter any license notices to the extent\n     required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n     You may choose to offer, and to charge a fee for, warranty, support,\n     indemnity or liability obligations to one or more recipients of Covered\n     Software. However, You may do so only on Your own behalf, and not on behalf\n     of any Contributor. You must make it absolutely clear that any such\n     warranty, support, indemnity, or liability obligation is offered by You\n     alone, and You hereby agree to indemnify every Contributor for any\n     liability incurred by such Contributor as a result of warranty, support,\n     indemnity or liability terms You offer. You may include additional\n     disclaimers of warranty and limitations of liability specific to any\n     jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n   If it is impossible for You to comply with any of the terms of this License\n   with respect to some or all of the Covered Software due to statute, judicial\n   order, or regulation then You must: (a) comply with the terms of this License\n   to the maximum extent possible; and (b) describe the limitations and the code\n   they affect. Such description must be placed in a text file included with all\n   distributions of the Covered Software under this License. Except to the\n   extent prohibited by statute or regulation, such description must be\n   sufficiently detailed for a recipient of ordinary skill to be able to\n   understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n     fail to comply with any of its terms. However, if You become compliant,\n     then the rights granted under this License from a particular Contributor\n     are reinstated (a) provisionally, unless and until such Contributor\n     explicitly and finally terminates Your grants, and (b) on an ongoing basis,\n     if such Contributor fails to notify You of the non-compliance by some\n     reasonable means prior to 60 days after You have come back into compliance.\n     Moreover, Your grants from a particular Contributor are reinstated on an\n     ongoing basis if such Contributor notifies You of the non-compliance by\n     some reasonable means, this is the first time You have received notice of\n     non-compliance with this License from such Contributor, and You become\n     compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n     infringement claim (excluding declaratory judgment actions, counter-claims,\n     and cross-claims) alleging that a Contributor Version directly or\n     indirectly infringes any patent, then the rights granted to You by any and\n     all Contributors for the Covered Software under Section 2.1 of this License\n     shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n     license agreements (excluding distributors and resellers) which have been\n     validly granted by You or Your distributors under this License prior to\n     termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n   Covered Software is provided under this License on an “as is” basis, without\n   warranty of any kind, either expressed, implied, or statutory, including,\n   without limitation, warranties that the Covered Software is free of defects,\n   merchantable, fit for a particular purpose or non-infringing. The entire\n   risk as to the quality and performance of the Covered Software is with You.\n   Should any Covered Software prove defective in any respect, You (not any\n   Contributor) assume the cost of any necessary servicing, repair, or\n   correction. This disclaimer of warranty constitutes an essential part of this\n   License. No use of  any Covered Software is authorized under this License\n   except under this disclaimer.\n\n7. Limitation of Liability\n\n   Under no circumstances and under no legal theory, whether tort (including\n   negligence), contract, or otherwise, shall any Contributor, or anyone who\n   distributes Covered Software as permitted above, be liable to You for any\n   direct, indirect, special, incidental, or consequential damages of any\n   character including, without limitation, damages for lost profits, loss of\n   goodwill, work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses, even if such party shall have been\n   informed of the possibility of such damages. This limitation of liability\n   shall not apply to liability for death or personal injury resulting from such\n   party’s negligence to the extent applicable law prohibits such limitation.\n   Some jurisdictions do not allow the exclusion or limitation of incidental or\n   consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation\n\n   Any litigation relating to this License may be brought only in the courts of\n   a jurisdiction where the defendant maintains its principal place of business\n   and such litigation shall be governed by laws of that jurisdiction, without\n   reference to its conflict-of-law provisions. Nothing in this Section shall\n   prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous\n\n   This License represents the complete agreement concerning the subject matter\n   hereof. If any provision of this License is held to be unenforceable, such\n   provision shall be reformed only to the extent necessary to make it\n   enforceable. Any law or regulation which provides that the language of a\n   contract shall be construed against the drafter shall not be used to construe\n   this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n      Mozilla Foundation is the license steward. Except as provided in Section\n      10.3, no one other than the license steward has the right to modify or\n      publish new versions of this License. Each version will be given a\n      distinguishing version number.\n\n10.2. Effect of New Versions\n\n      You may distribute the Covered Software under the terms of the version of\n      the License under which You originally received the Covered Software, or\n      under the terms of any subsequent version published by the license\n      steward.\n\n10.3. Modified Versions\n\n      If you create software not governed by this License, and you want to\n      create a new license for such software, you may create and use a modified\n      version of this License if you rename the license and remove any\n      references to the name of the license steward (except to note that such\n      modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses\n      If You choose to distribute Source Code Form that is Incompatible With\n      Secondary Licenses under the terms of this version of the License, the\n      notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n\n      This Source Code Form is subject to the\n      terms of the Mozilla Public License, v.\n      2.0. If a copy of the MPL was not\n      distributed with this file, You can\n      obtain one at\n      http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then\nYou may include the notice in a location (such as a LICENSE file in a relevant\ndirectory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice\n\n      This Source Code Form is “Incompatible\n      With Secondary Licenses”, as defined by\n      the Mozilla Public License, v. 2.0.\n\n\n# fs\n\nPackage fs provides filesystem-related functions.\n\nhttp://github.com/kr/fs\n\n\nCopyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n# properties\n\nproperties is a Go library for reading and writing properties files.\n\nhttp://github.com/magiconair/properties\n\n\ngoproperties - properties file decoder for Go\n\nCopyright (c) 2013-2014 - Frank Schroeder\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n# mapstructure\n\nGo library for decoding generic map values into native Go structures.\n\nhttp://github.com/mitchellh/mapstructure\n\n\nThe MIT License (MIT)\n\nCopyright (c) 2013 Mitchell Hashimoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n\n# Go-toml\n\nGo library for the TOML language\n\nhttp://github.com/pelletier/go-toml\n\n\nThe MIT License (MIT)\n\nCopyright (c) 2013 - 2016 Thomas Pelletier, Eric Anderton\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n# errors\n\nSimple error handling primitives\n\nhttp://github.com/pkg/errors\n\n\nCopyright (c) 2015, Dave Cheney <dave@cheney.net>\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n# sftp\n\nSFTP support for the go.crypto/ssh package\n\nhttp://github.com/pkg/sftp\n\n\nCopyright (c) 2013, Dave Cheney\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n\n# frigga-go\n\nA selective Golang port of Netflix's Frigga project.\n\nhttp://github.com/SmartThingsOSS/frigga-go\n\n\nCopyright 2015 SmartThings, Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n\n# Afero\n\nA FileSystem Abstraction System for Go\n\nhttp://github.com/spf13/afero\n\n\n                                Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n\n# cast\n\nEasy and safe casting from one type to another in Go\n\nhttp://github.com/spf13/cast\n\n\nThe MIT License (MIT)\n\nCopyright (c) 2014 Steve Francia\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\n\n# jWalterWeatherman\n\nSeamless printing to the terminal (stdout) and logging to a io.Writer (file)\nthat’s as easy to use as fmt.Println.\n\n\nhttp://github.com/spf13/jwalterweatherman\n\n\nThe MIT License (MIT)\n\nCopyright (c) 2014 Steve Francia\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\n# pflag\n\npflag is a drop-in replacement for Go's flag package, implementing POSIX/GNU-style --flags.\n\n\nhttp://github.com/spf13/pflag\n\n\nCopyright (c) 2012 Alex Ogier. All rights reserved.\nCopyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n# viper\n\nGo configuration with fangs\n\n\nhttp://github.com/spf13/viper\n\n\nThe MIT License (MIT)\n\nCopyright (c) 2014 Steve Francia\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\n# crypto\n\nGo supplementary cryptography libraries\n\nhttp://golang.org/x/crypto\n\n\nCopyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n# sys\n\nGo packages for low-level interaction with the operating system\n\nhttp://golang.org/x/sys\n\n\nCopyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n# text\n\nGo text processing support\n\n\nhttp://golang.org/x/text\n\n\nCopyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n# yaml\n\nYAML support for the Go language\n\nhttps://github.com/go-yaml/yaml\n\nCopyright 2011-2016 Canonical Ltd.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n\nThe following files were ported to Go from C files of libyaml, and thus\nare still covered by their original copyright and license:\n\n    apic.go\n    emitterc.go\n    parserc.go\n    readerc.go\n    scannerc.go\n    writerc.go\n    yamlh.go\n    yamlprivateh.go\n\nCopyright (c) 2006 Kirill Simonov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\n# sql-migrate\n\nSQL Schema migration tool for Go\n\nhttps://github.com/rubenv/sql-migrate\n\n(The MIT License)\n\nCopyright (C) 2014-2016 by Ruben Vermeersch <ruben@rocketeer.be>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n\n# gorp\n\nGo Relational Persistence\n\nhttps://github.com/go-gorp/gorp\n\n(The MIT License)\n\nCopyright (c) 2012 James Cooper <james@bitmechanic.com>\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\n"
  },
  {
    "path": "OSSMETADATA",
    "content": "osslifecycle=active\n"
  },
  {
    "path": "README.md",
    "content": "![logo](docs/logo.png \"logo\")\n\n[![NetflixOSS Lifecycle](https://img.shields.io/osslifecycle/Netflix/chaosmonkey.svg)](OSSMETADATA) [![Build Status][travis-badge]][travis] [![GoDoc][godoc-badge]][godoc] [![GoReportCard][report-badge]][report]\n\n[travis-badge]: https://travis-ci.com/Netflix/chaosmonkey.svg?branch=master\n[travis]: https://travis-ci.com/Netflix/chaosmonkey\n[godoc-badge]: https://godoc.org/github.com/Netflix/chaosmonkey?status.svg\n[godoc]: https://godoc.org/github.com/Netflix/chaosmonkey\n[report-badge]: https://goreportcard.com/badge/github.com/Netflix/chaosmonkey\n[report]: https://goreportcard.com/report/github.com/Netflix/chaosmonkey\n\nChaos Monkey randomly terminates virtual machine instances and containers that\nrun inside of your production environment. Exposing engineers to\nfailures more frequently incentivizes them to build resilient services.\n\nSee the [documentation][docs] for info on how to use Chaos Monkey.\n\nChaos Monkey is an example of a tool that follows the\n[Principles of Chaos Engineering][PoC].\n\n[PoC]: http://principlesofchaos.org/\n\n### Requirements\n\nThis version of Chaos Monkey is fully integrated with [Spinnaker], the\ncontinuous delivery platform that we use at Netflix. You must be managing your\napps with Spinnaker to use Chaos Monkey to terminate instances.\n\nChaos Monkey should work with any backend that Spinnaker supports (AWS, Google\nCompute Engine, Azure, Kubernetes, Cloud Foundry). It has been tested with\nAWS, [GCE][gce-blogpost], and Kubernetes.\n\n### Install locally\n\nTo install the Chaos Monkey binary on your local machine:\n\n```\ngo get github.com/netflix/chaosmonkey/cmd/chaosmonkey\n```\n\n### How to deploy\n\nSee the [docs] for instructions on how to configure and deploy Chaos Monkey.\n\n### Support\n\n[Simian Army Google group](http://groups.google.com/group/simianarmy-users).\n\n[Spinnaker]: http://www.spinnaker.io/\n[docs]: https://netflix.github.io/chaosmonkey\n[gce-blogpost]: https://medium.com/continuous-delivery-scale/running-chaos-monkey-on-spinnaker-google-compute-engine-gce-155dc52f20ef\n"
  },
  {
    "path": "cal/cal.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package cal has calendar-related functions\npackage cal\n\nimport \"fmt\"\nimport \"time\"\n\n// IsWorkday returns true if the date associated with t is a work day\n// Uses the location associated with t to make this calculation\nfunc IsWorkday(t time.Time) bool {\n\treturn isWeekday(t)\n}\n\nfunc isWeekday(t time.Time) bool {\n\tswitch t.Weekday() {\n\tcase time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday:\n\t\treturn true\n\tcase time.Saturday, time.Sunday:\n\t\treturn false\n\t}\n\n\tpanic(fmt.Sprintf(\"Unknown weekday: %s\", t.Weekday()))\n}\n"
  },
  {
    "path": "cal/cal_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cal_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/cal\"\n)\n\nvar weekdayTests = []struct {\n\tdate string\n\tpred bool\n}{\n\t{\"Mon Dec 14 11:33:58 PST 2015\", true},\n\t{\"Tue Dec 15 11:33:58 PST 2015\", true},\n\t{\"Wed Dec 16 11:33:58 PST 2015\", true},\n\t{\"Thu Dec 17 11:33:58 PST 2015\", true},\n\t{\"Fri Dec 18 11:33:58 PST 2015\", true},\n\t{\"Sat Dec 19 11:33:58 PST 2015\", false},\n\t{\"Sun Dec 20 11:33:58 PST 2015\", false},\n}\n\nfunc TestIsWorkday(t *testing.T) {\n\tfor _, tt := range weekdayTests {\n\t\tif got, want := cal.IsWorkday(parse(tt.date)), tt.pred; got != want {\n\t\t\tt.Fatalf(\"isWeekday(\\\"%s\\\")=%t, want %t\", tt.date, got, want)\n\t\t}\n\t}\n}\n\n// parse returns a time formatted as the standard output of \"date\", e.g.:\n// Thu Dec 17 15:18:30 PST 2015\nfunc parse(s string) time.Time {\n\tt, err := time.Parse(\"Mon Jan  2 15:04:05 PST 2006\", s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn t\n}\n"
  },
  {
    "path": "chaosmonkey.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package chaosmonkey contains our domain models\npackage chaosmonkey\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\nconst (\n\t// App grouping: Chaos Monkey kills one instance per app per day\n\tApp Group = iota\n\t// Stack grouping: Chaos Monkey kills one instance per stack per day\n\tStack\n\t// Cluster grouping: Chaos Monkey kills one instance per cluster per day\n\tCluster\n)\n\ntype (\n\n\t// AppConfig contains app-specific configuration parameters for Chaos Monkey\n\tAppConfig struct {\n\t\tEnabled                        bool\n\t\tRegionsAreIndependent          bool\n\t\tMeanTimeBetweenKillsInWorkDays int\n\t\tMinTimeBetweenKillsInWorkDays  int\n\t\tGrouping                       Group\n\t\tExceptions                     []Exception\n\t\tWhitelist                      *[]Exception\n\t}\n\n\t// Group describes what Chaos Monkey considers a group of instances\n\t// Chaos Monkey will randomly kill an instance from each group.\n\t// The group generally maps onto what the service owner considers\n\t// a \"cluster\", which is different from Spinnaker's notion of a cluster.\n\tGroup int\n\n\t// Exception describes clusters that have been opted out of chaos monkey\n\t// If one of the members is a \"*\", it matches everything. That is the only\n\t// wildcard value\n\t// For example, this will opt-out all of the cluters in the test account:\n\t// Exception{ Account:\"test\", Stack:\"*\", Cluster:\"*\", Region: \"*\"}\n\tException struct {\n\t\tAccount string\n\t\tStack   string\n\t\tDetail  string\n\t\tRegion  string\n\t}\n\n\t// Instance contains naming info about an instance\n\tInstance interface {\n\t\t// AppName is the name of the Netflix app\n\t\tAppName() string\n\n\t\t// AccountName is the name of the account the instance is running in (e.g., prod, test)\n\t\tAccountName() string\n\n\t\t// RegionName is the name of the AWS region (e.g., us-east-1\n\t\tRegionName() string\n\n\t\t// StackName returns the \"stack\" part of app-stack-detail in cluster names\n\t\tStackName() string\n\n\t\t// ClusterName is the full cluster name: app-stack-detail\n\t\tClusterName() string\n\n\t\t// ASGName is the name of the ASG associated with the instance\n\t\tASGName() string\n\n\t\t// ID is the instance ID, e.g. i-dbcba24c\n\t\tID() string\n\n\t\t// CloudProvider returns the cloud provider (e.g., \"aws\")\n\t\tCloudProvider() string\n\t}\n\n\t// Termination contains information about an instance termination.\n\tTermination struct {\n\t\tInstance Instance  // The instance that will be terminated\n\t\tTime     time.Time // Termination time\n\t\tLeashed  bool      // If true, track the termination but do not execute it\n\t}\n\n\t// Tracker records termination events an a tracking system such as Chronos\n\tTracker interface {\n\t\t// Track pushes a termination event to the tracking system\n\t\tTrack(t Termination) error\n\t}\n\n\t// ErrorCounter counts when errors occur.\n\tErrorCounter interface {\n\t\tIncrement() error\n\t}\n\n\t// Decryptor decrypts encrypted text. It is used for decrypting\n\t// sensitive credentials that are stored encrypted\n\tDecryptor interface {\n\t\tDecrypt(ciphertext string) (string, error)\n\t}\n\n\t// Env provides information about the environment that Chaos Monkey has been\n\t// deployed to.\n\tEnv interface {\n\t\t// InTest returns true if Chaos Monkey is running in a test environment\n\t\tInTest() bool\n\t}\n\n\t// AppConfigGetter retrieves App configuration info\n\tAppConfigGetter interface {\n\t\t// Get returns the App config info by app name\n\t\tGet(app string) (*AppConfig, error)\n\t}\n\n\t// Checker checks to see if a termination is permitted given min time between terminations\n\t//\n\t// if the termination is permitted, returns (true, nil)\n\t// otherwise, returns false with an error\n\t//\n\t// Returns ErrViolatesMinTime if violates min time between terminations\n\t//\n\t// Note that this call may change the state of the server: if the checker returns true, the termination will be recorded.\n\tChecker interface {\n\t\t// Check checks if a termination is permitted and, if so, records the\n\t\t// termination time on the server.\n\t\t// The endHour (hour time when Chaos Monkey stops killing) is in the\n\t\t// time zone specified by loc.\n\t\tCheck(term Termination, appCfg AppConfig, endHour int, loc *time.Location) error\n\t}\n\n\t// Terminator provides an interface for killing instances\n\tTerminator interface {\n\t\t// Kill terminates a running instance\n\t\tExecute(trm Termination) error\n\t}\n\n\t// Outage provides an interface for checking if there is currently an outage\n\t// This provides a mechanism to check if there's an ongoing outage, since\n\t// Chaos Monkey doesn't run during outages\n\tOutage interface {\n\t\t// Outage returns true if there is an ongoing outage\n\t\tOutage() (bool, error)\n\t}\n\n\t// ErrViolatesMinTime represents an error when trying to record a termination\n\t// that violates the min time between terminations for that particular app\n\tErrViolatesMinTime struct {\n\t\tInstanceID string         // the most recent terminated instance id\n\t\tKilledAt   time.Time      // the time that the most recent instance was terminated\n\t\tLoc        *time.Location // local time zone location\n\t}\n)\n\n// String returns a string representation for a Group\nfunc (g Group) String() string {\n\tswitch g {\n\tcase App:\n\t\treturn \"app\"\n\tcase Stack:\n\t\treturn \"stack\"\n\tcase Cluster:\n\t\treturn \"cluster\"\n\t}\n\n\tpanic(\"Unknown Group value\")\n}\n\n// NewAppConfig constructs a new app configuration with reasonable defaults\n// with specified accounts enabled/disabled\nfunc NewAppConfig(exceptions []Exception) AppConfig {\n\tresult := AppConfig{\n\t\tEnabled:                        true,\n\t\tRegionsAreIndependent:          true,\n\t\tMeanTimeBetweenKillsInWorkDays: 5,\n\t\tGrouping:                       Cluster,\n\t\tExceptions:                     exceptions,\n\t}\n\n\treturn result\n}\n\n// Matches returns true if an exception matches an ASG\nfunc (ex Exception) Matches(account, stack, detail, region string) bool {\n\treturn exFieldMatches(ex.Account, account) &&\n\t\texFieldMatches(ex.Stack, stack) &&\n\t\texFieldMatches(ex.Detail, detail) &&\n\t\texFieldMatches(ex.Region, region)\n}\n\n// exFieldMatches checks if an exception field matches a given value\n// It's true if field is \"*\" or if the field is the same string as the value\nfunc exFieldMatches(field, value string) bool {\n\treturn field == \"*\" || field == value\n}\n\nfunc (e ErrViolatesMinTime) Error() string {\n\ts := fmt.Sprintf(\"Would violate min between kills: instance %s was killed at %s\", e.InstanceID, e.KilledAt)\n\n\t// If we know the time zone, report that as well\n\tif e.Loc != nil {\n\t\ts += fmt.Sprintf(\" (%s)\", e.KilledAt.In(e.Loc))\n\t}\n\n\treturn s\n}\n"
  },
  {
    "path": "chaosmonkey_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage chaosmonkey_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n)\n\nfunc TestExceptionMatches(t *testing.T) {\n\tex := chaosmonkey.Exception{Account: \"test\", Stack: \"*\", Detail: \"*\", Region: \"*\"}\n\n\tif !ex.Matches(\"test\", \"cl\", \"app-cl-test\", \"us-east-1\") {\n\t\tt.Error(\"Expected exception match\")\n\t}\n}\n"
  },
  {
    "path": "clock/clock.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package clock provides the Clock interface for getting the current time\npackage clock\n\nimport \"time\"\n\n// Clock provides an interface to the current time, useful for testing\ntype Clock interface {\n\t// Now returns the current time\n\tNow() time.Time\n}\n\n// New returns an implementation of Clock that uses the system time\nfunc New() Clock {\n\treturn SystemClock{}\n}\n\n// SystemClock uses the system clock to return the time\ntype SystemClock struct{}\n\n// Now implements Clock.Now\nfunc (cl SystemClock) Now() time.Time {\n\treturn time.Now()\n}\n"
  },
  {
    "path": "cmd/chaosmonkey/main.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n/*\nChaos Monkey randomly terminates instances.\n*/\npackage main\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/v2/command\"\n\n\t// These are anonymous imported so that the related Get* methods (e.g.,\n\t// GetDecryptor) are picked up.\n\n\t_ \"github.com/Netflix/chaosmonkey/v2/constrainer\"\n\t_ \"github.com/Netflix/chaosmonkey/v2/decryptor\"\n\t_ \"github.com/Netflix/chaosmonkey/v2/env\"\n\t_ \"github.com/Netflix/chaosmonkey/v2/errorcounter\"\n\t_ \"github.com/Netflix/chaosmonkey/v2/outage\"\n\t_ \"github.com/Netflix/chaosmonkey/v2/tracker\"\n)\n\nfunc main() {\n\tcommand.Execute()\n}\n"
  },
  {
    "path": "command/chaosmonkey.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"math\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"time\"\n\n\tflag \"github.com/spf13/pflag\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/clock\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/config/param\"\n\t\"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n\t\"github.com/Netflix/chaosmonkey/v2/mysql\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedstore\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedule\"\n\t\"github.com/Netflix/chaosmonkey/v2/spinnaker\"\n)\n\n// Version is the version number\nconst Version = \"2.0.2\"\n\nfunc printVersion() {\n\tfmt.Printf(\"%s\\n\", Version)\n}\n\nvar (\n\t// configPaths is where Chaos Monkey will look for a chaosmonkey.toml\n\t// configuration file\n\tconfigPaths = [...]string{\".\", \"/apps/chaosmonkey\", \"/etc\", \"/etc/chaosmonkey\"}\n)\n\n// Usage prints usage\nfunc Usage() {\n\tusage := `\nChaos Monkey\n\nUsage:\n\tchaosmonkey <command> ...\n\ncommand: migrate | schedule | terminate | fetch-schedule | outage | config  | email | eligible | intest\n\nInstall\n-------\nInstalls chaosmonkey with all the setup required, e.g setting up the cron, appling database migration etc.\n\nmigrate\n-------\nApplies database migration to the database defined in the configuration file.\n\nschedule [--max-apps=<N>] [--apps=foo,bar,baz] [--no-record-schedule]\n--------------------------------------------------------------------\nGenerates a schedule of terminations for the day and installs the\nterminations as local cron jobs that call \"chaosmonkey terminate ...\"\n\n--apps=foo,bar,baz     Optionally specify an explicit list of apps to schedule.\n                       This is primarily used for debugging.\n\n--max-apps=<N>         Optionally specify the maximum number of apps that Chaos Monkey\n\t\t\t\t\t   will schedule. This is primarily used for debugging.\n\n--no-record-schedule   Do not record the schedule with the database.\n                       This is primarily used for debugging.\n\n\nterminate <app> <account> [--region=<region>] [--stack=<stack>] [--cluster=<cluster>] [--leashed]\n-----------------------------------------------------------------------------------------------------------------\nTerminates an instance from a given app and account.\n\nOptionally specify a region, stack, cluster.\n\nThe --leashed flag forces chaosmonkey to run in leashed mode. When leashed,\nChaos Monkey will check if an instance should be terminated, but will not\nactually terminate it.\n\nfetch-schedule\n--------------\nQueries the database to see if there is an existing schedule of\nterminations for today. If so, downloads the schedule and sets up cron jobs to\nimplement the schedule.\n\noutage\n------\nOutput \"true\" if there is an ongoing outage, otherwise \"false\". Used for debugging.\n\n\nconfig [<app>]\n------------\nQuery Spinnaker for the config for a specific app and dump it to\nstandard out. This is only used for debugging.\n\nIf no app is specified, dump the Monkey-level configuration options to standard out.\n\nExamples:\n\n\tchaosmonkey config chaosguineapig\n\n\tchaosmonkey config\n\neligible <app> <account> [--region=<region>] [--stack=<stack>] [--cluster=<cluster>]\n-------------------------------------------------------------------------------------\n\nDump a list of instance-ids that are eligible for termination for a given app, account,\nand optionally region, stack, and cluster.\n\nintest\n------\n\nOutputs \"true\" on standard out if running within a test environment, otherwise outputs \"false\"\n\n\naccount <name>\n--------------\n\nLook up an cloud account ID by name.\n\nExample:\n\n\tchaosmonkey account test\n\n\nprovider <name>\n---------------\n\nLook up the cloud provider by account name.\n\nExample:\n\n\tchaosmonkey provider test\n\n\nclusters <app> <account>\n------------------------\n\nList the clusters for a given app and account\n\nExample:\n\n\tchaosmonkey clusters chaosguineapig test\n\n\nregions <cluster> <account>\n---------------------------\n\nList the regions for a given cluster and account\n\nExample:\n\n\tchaosmonkey regions chaosguineapig test\n`\n\tfmt.Printf(usage)\n}\n\nfunc init() {\n\t// Prepend the pid to log statements\n\tlog.SetPrefix(fmt.Sprintf(\"[%5d] \", os.Getpid()))\n}\n\n// Execute is the main entry point for the chaosmonkey cli.\nfunc Execute() {\n\tregionPtr := flag.String(\"region\", \"\", \"region of termination group\")\n\tstackPtr := flag.String(\"stack\", \"\", \"stack of termination group\")\n\tclusterPtr := flag.String(\"cluster\", \"\", \"cluster of termination group\")\n\tappsPtr := flag.String(\"apps\", \"\", \"comma-separated list of apps to schedule for termination\")\n\tnoRecordSchedulePtr := flag.Bool(\"no-record-schedule\", false, \"do not record schedule\")\n\tversionPtr := flag.BoolP(\"version\", \"v\", false, \"show version\")\n\tflag.Usage = Usage\n\n\t// These flags, if specified, override config values\n\tmaxAppsFlag := \"max-apps\"\n\tleashedFlag := \"leashed\"\n\tflag.Int(maxAppsFlag, math.MaxInt32, \"max number of apps to examine for termination\")\n\tflag.Bool(leashedFlag, false, \"force leashed mode\")\n\n\tflag.Parse()\n\tif len(flag.Args()) == 0 {\n\t\tif *versionPtr {\n\t\t\tprintVersion()\n\t\t\tos.Exit(0)\n\t\t}\n\n\t\tflag.Usage()\n\t\tos.Exit(1)\n\t}\n\n\tcmd := flag.Arg(0)\n\n\tcfg, err := getConfig()\n\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: failed to load config: %v\", err)\n\t}\n\n\t// Associate config values with flags\n\terr = cfg.BindPFlag(param.MaxApps, flag.Lookup(maxAppsFlag))\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: failed to bind flag: --%s: %v\", maxAppsFlag, err)\n\t}\n\terr = cfg.BindPFlag(param.Leashed, flag.Lookup(leashedFlag))\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: failed to bind flag: --%s: %v\", leashedFlag, err)\n\t}\n\n\tspin, err := spinnaker.NewFromConfig(cfg)\n\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: spinnaker.New failed: %+v\", err)\n\t}\n\n\toutage, err := deps.GetOutage(cfg)\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: deps.GetOutage fail: %+v\", err)\n\t}\n\n\tsql, err := mysql.NewFromConfig(cfg)\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: could not initialize mysql connection: %+v\", err)\n\t}\n\n\tcons, err := deps.GetConstrainer(cfg)\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: deps.GetConstrainer failed: %+v\", err)\n\t}\n\n\t// Ensure mysql object gets closed\n\tdefer func() {\n\t\t_ = sql.Close()\n\t}()\n\n\tswitch cmd {\n\tcase \"install\":\n\t\texecutable := ChaosmonkeyExecutable{}\n\t\tInstall(cfg, executable, sql)\n\tcase \"migrate\":\n\t\tMigrate(sql)\n\tcase \"schedule\":\n\t\tlog.Println(\"chaosmonkey schedule starting\")\n\t\tdefer log.Println(\"chaosmonkey schedule done\")\n\n\t\tvar apps []string\n\t\tif *appsPtr != \"\" {\n\t\t\t// User explicitly specified list of apps on the command line\n\t\t\tapps = strings.Split(*appsPtr, \",\")\n\t\t} else {\n\t\t\t// User did not explicitly specify list of apps, get 'em all\n\t\t\tvar err error\n\t\t\tapps, err = spin.AppNames()\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"FATAL: could not retrieve list of app names: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\tvar schedStore schedstore.SchedStore\n\n\t\tschedStore = sql\n\t\tif *noRecordSchedulePtr {\n\t\t\tschedStore = nullSchedStore{}\n\t\t}\n\n\t\tSchedule(spin, schedStore, cfg, spin, cons, apps)\n\tcase \"fetch-schedule\":\n\t\tFetchSchedule(sql, cfg)\n\tcase \"terminate\":\n\t\tif len(flag.Args()) != 3 {\n\t\t\tflag.Usage()\n\t\t\tos.Exit(1)\n\t\t}\n\t\tapp := flag.Arg(1)\n\t\taccount := flag.Arg(2)\n\t\ttrackers, err := deps.GetTrackers(cfg)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"FATAL: could not create trackers: %+v\", err)\n\t\t}\n\n\t\terrCounter, err := deps.GetErrorCounter(cfg)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"FATAL: could not create error counter: %+v\", err)\n\t\t}\n\n\t\tenv, err := deps.GetEnv(cfg)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"FATAL: could not determine environment: %+v\", err)\n\t\t}\n\n\t\tdefer logOnPanic(errCounter) // Handler in case of panic\n\t\tdeps := deps.Deps{\n\t\t\tMonkeyCfg:  cfg,\n\t\t\tChecker:    sql,\n\t\t\tConfGetter: spin,\n\t\t\tCl:         clock.New(),\n\t\t\tDep:        spin,\n\t\t\tT:          spin,\n\t\t\tTrackers:   trackers,\n\t\t\tOu:         outage,\n\t\t\tErrCounter: errCounter,\n\t\t\tEnv:        env,\n\t\t}\n\t\tTerminate(deps, app, account, *regionPtr, *stackPtr, *clusterPtr)\n\tcase \"outage\":\n\t\tOutage(outage)\n\tcase \"config\":\n\t\tif len(flag.Args()) != 2 {\n\t\t\tDumpMonkeyConfig(cfg)\n\t\t\treturn\n\t\t}\n\t\tapp := flag.Arg(1)\n\t\tDumpConfig(spin, app)\n\tcase \"eligible\":\n\t\tif len(flag.Args()) != 3 {\n\t\t\tflag.Usage()\n\t\t\tos.Exit(1)\n\t\t}\n\t\tapp := flag.Arg(1)\n\t\taccount := flag.Arg(2)\n\t\tEligible(spin, spin, app, account, *regionPtr, *stackPtr, *clusterPtr)\n\tcase \"intest\":\n\t\tenv, err := deps.GetEnv(cfg)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"FATAL: could not determine environment: %+v\", err)\n\t\t}\n\t\tfmt.Println(env.InTest())\n\tcase \"account\":\n\t\tif len(flag.Args()) != 2 {\n\t\t\tflag.Usage()\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\taccount := flag.Arg(1)\n\t\tid, err := spin.AccountID(account)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"ERROR: Could not retrieve id for account: %s. Reason: %v\\n\", account, err)\n\t\t\treturn\n\t\t}\n\t\tfmt.Println(id)\n\tcase \"provider\":\n\t\tif len(flag.Args()) != 2 {\n\t\t\tflag.Usage()\n\t\t\tos.Exit(1)\n\t\t}\n\t\taccount := flag.Arg(1)\n\t\tprovider, err := spin.CloudProvider(account)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"ERROR: Could not retrieve provider for account: %s. Reason: %v\\n\", account, err)\n\t\t\treturn\n\t\t}\n\t\tfmt.Println(provider)\n\tcase \"clusters\":\n\t\tif len(flag.Args()) != 3 {\n\t\t\tflag.Usage()\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tapp := flag.Arg(1)\n\t\taccount := flag.Arg(2)\n\t\tclusters, err := spin.GetClusterNames(app, deploy.AccountName(account))\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"ERROR: %v\\n\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tfor _, cluster := range clusters {\n\t\t\tfmt.Println(cluster)\n\t\t}\n\n\tcase \"regions\":\n\t\tif len(flag.Args()) != 3 {\n\t\t\tflag.Usage()\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tcluster := flag.Arg(1)\n\t\taccount := flag.Arg(2)\n\n\t\tDumpRegions(cluster, account, spin)\n\n\tdefault:\n\t\tflag.Usage()\n\t\tos.Exit(1)\n\t}\n}\n\nfunc init() {\n\t// All logs to stdout\n\tlog.SetOutput(os.Stdout)\n}\n\n// logOnPanic increments an error metric and logs if a panic happens\nfunc logOnPanic(errCounter chaosmonkey.ErrorCounter) {\n\tif e := recover(); e != nil {\n\t\tlog.Printf(\"FATAL: panic: %s: %s\", e, debug.Stack())\n\t\terr := errCounter.Increment()\n\t\tif err != nil {\n\t\t\tlog.Printf(\"failed to increment error counter: %s\", err)\n\t\t}\n\t}\n}\n\n// return configuration info\nfunc getConfig() (*config.Monkey, error) {\n\tcfg, err := config.Load(configPaths[:])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cfg, nil\n}\n\n// nullSchedStore is a no-op implementation of api.SchedStore\ntype nullSchedStore struct{}\n\n// Retrieve implements api.SchedStore.Retrieve\nfunc (n nullSchedStore) Retrieve(date time.Time) (*schedule.Schedule, error) {\n\treturn nil, fmt.Errorf(\"nullSchedStore does not support Retrieve function\")\n}\n\n// Publish implements api.SchedStore.Publish\nfunc (n nullSchedStore) Publish(date time.Time, sched *schedule.Schedule) error {\n\treturn nil\n}\n"
  },
  {
    "path": "command/command.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package command contains functions that can be invoked via command-line\n// e.g. \"chaosmonkey schedule\" invokes command.Schedule\npackage command\n"
  },
  {
    "path": "command/dumpconfig.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/davecgh/go-spew/spew\"\n)\n\n// DumpConfig dumps the config for an app to stdout\nfunc DumpConfig(c chaosmonkey.AppConfigGetter, app string) {\n\tcfg, err := c.Get(app)\n\tif err != nil {\n\t\tfmt.Printf(\"%+v\", err)\n\t\tos.Exit(1)\n\t}\n\n\tspew.Dump(cfg)\n}\n"
  },
  {
    "path": "command/dumpmonkeyconfig.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n)\n\n// DumpMonkeyConfig dumps the monkey-level config parameters to stdout\nfunc DumpMonkeyConfig(cfg *config.Monkey) {\n\tvar enabled, leashed, sched bool\n\tvar accounts []string\n\tvar err error\n\n\tif enabled, err = cfg.Enabled(); err != nil {\n\t\tfmt.Printf(\"ERROR getting enabled: %v\", err)\n\t} else {\n\t\tfmt.Printf(\"enabled: %t\\n\", enabled)\n\t}\n\n\tif leashed, err = cfg.Leashed(); err != nil {\n\t\tfmt.Printf(\"ERROR getting leashed: %v\", err)\n\t} else {\n\t\tfmt.Printf(\"leashed: %t\\n\", leashed)\n\t}\n\n\tif sched, err = cfg.ScheduleEnabled(); err != nil {\n\t\tfmt.Printf(\"ERROR getting schedule enabled: %v\", err)\n\t} else {\n\t\tfmt.Printf(\"schedule enabled: %t\\n\", sched)\n\t}\n\n\tif accounts, err = cfg.Accounts(); err != nil {\n\t\tfmt.Printf(\"ERROR getting accounts: %v\\n\", err)\n\t} else {\n\t\tfmt.Printf(\"accounts: %v\\n\", accounts)\n\t}\n\n\tfmt.Printf(\"start hour: %d\\n\", cfg.StartHour())\n\tfmt.Printf(\"end hour: %d\\n\", cfg.EndHour())\n\tloc, _ := cfg.Location()\n\tfmt.Printf(\"location: %s\\n\", loc)\n\tfmt.Printf(\"cron path: %s\\n\", cfg.CronPath())\n\tfmt.Printf(\"term path: %s\\n\", cfg.TermPath())\n\tfmt.Printf(\"term account: %s\\n\", cfg.TermAccount())\n\tfmt.Printf(\"max apps: %d\\n\", cfg.MaxApps())\n}\n"
  },
  {
    "path": "command/eligible.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/eligible\"\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n)\n\n// Eligible prints out a list of instance ids eligible for termination\n// It is intended only for testing\nfunc Eligible(g chaosmonkey.AppConfigGetter, d deploy.Deployment, app, account, region, stack, cluster string) {\n\tcfg, err := g.Get(app)\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to retrieve config for app %s\\n%+v\", app, err)\n\t\tos.Exit(1)\n\t}\n\n\tgroup := grp.New(app, account, region, stack, cluster)\n\tinstances, err := eligible.Instances(group, cfg.Exceptions, d)\n\tif err != nil {\n\t\tfmt.Print(err)\n\t\tos.Exit(1)\n\t}\n\n\tfor _, instance := range instances {\n\t\tfmt.Println(instance.ID())\n\t}\n}\n"
  },
  {
    "path": "command/fetchschedule.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedstore\"\n)\n\n// FetchSchedule executes the \"fetch-schedule\" command. This checks if there\n// is an existing schedule for today that was previously registered\n// in chaosmonkey-api. If so, it downloads the schedule from chaosmonkey-api\n// and installs it locally.\nfunc FetchSchedule(s schedstore.SchedStore, cfg *config.Monkey) {\n\tlog.Println(\"chaosmonkey fetch-schedule starting\")\n\tsched, err := s.Retrieve(today(cfg))\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: could not fetch schedule: %v\", err)\n\t}\n\n\tif sched == nil {\n\t\tlog.Println(\"no schedule to retrieve\")\n\t\treturn\n\t}\n\n\terr = registerWithCron(sched, cfg)\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: could not register with cron: %v\", err)\n\t}\n\n\tdefer log.Println(\"chaosmonkey fetch-schedule done\")\n}\n\n// today returns a date in local time\nfunc today(cfg *config.Monkey) time.Time {\n\tloc, err := cfg.Location()\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: Could not get local timezone: %v\", err)\n\t}\n\n\treturn time.Now().In(loc)\n}\n"
  },
  {
    "path": "command/install.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"fmt\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/mysql\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n)\n\nconst (\n\tscheduleCommand  = \"schedule\"\n\tterminateCommand = \"terminate\"\n\tscriptContent    = `#!/bin/bash\n%s %s \"$@\" >> %s/chaosmonkey-%s.log 2>&1\n`\n)\n\n// CurrentExecutable provides an interface to extract information about the current executable\ntype CurrentExecutable interface {\n\t// ExecutablePath returns the path to current executable\n\tExecutablePath() (string, error)\n}\n\n// Install installs chaosmonkey and runs database migration\nfunc Install(cfg *config.Monkey, exec CurrentExecutable, db mysql.MySQL) {\n\tInstallCron(cfg, exec)\n\tMigrate(db)\n\tlog.Println(\"installation done!\")\n}\n\n// InstallCron installs chaosmonkey schedule generation cron\nfunc InstallCron(cfg *config.Monkey, exec CurrentExecutable) {\n\texecutablePath, err := exec.ExecutablePath()\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: %v\", err)\n\t}\n\terr = setupTerminationScript(cfg, executablePath)\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: %v\", err)\n\t}\n\n\terr = setupCron(cfg, executablePath)\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: %v\", err)\n\t}\n\n\tlog.Println(\"chaosmonkey cron is installed successfully\")\n}\n\nfunc setupCron(cfg *config.Monkey, executablePath string) error {\n\terr := EnsureFileAbsent(cfg.SchedulePath())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = EnsureFileAbsent(cfg.ScheduleCronPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar scriptPerms os.FileMode = 0755 // -rwx-rx--rx-- : scripts should be executable\n\tlog.Printf(\"Creating %s\\n\", cfg.SchedulePath())\n\n\tcontent, err := generateScriptContent(scheduleCommand, cfg, executablePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = ioutil.WriteFile(cfg.SchedulePath(), content, scriptPerms)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcronExpr, err := cfg.CronExpression()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcrontab := fmt.Sprintf(\"%s %s %s\\n\", cronExpr, cfg.TermAccount(), cfg.SchedulePath())\n\tvar cronPerms os.FileMode = 0644 // -rw-r--r-- : cron config file shouldn't have write perm\n\tlog.Printf(\"Creating %s\\n\", cfg.ScheduleCronPath())\n\terr = ioutil.WriteFile(cfg.ScheduleCronPath(), []byte(crontab), cronPerms)\n\treturn err\n}\n\nfunc setupTerminationScript(cfg *config.Monkey, executablePath string) error {\n\terr := EnsureFileAbsent(cfg.TermPath())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar perms os.FileMode = 0755 // -rwx-rx--rx-- : scripts should be executable\n\tlog.Printf(\"Creating %s\\n\", cfg.TermPath())\n\n\tcontent, err := generateScriptContent(terminateCommand, cfg, executablePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = ioutil.WriteFile(cfg.TermPath(), content, perms)\n\treturn err\n}\n\nfunc generateScriptContent(cmdName string, cfg *config.Monkey, executablePath string) ([]byte, error) {\n\tcontent := fmt.Sprintf(scriptContent, executablePath, cmdName, cfg.LogPath(), cmdName)\n\treturn []byte(content), nil\n}\n"
  },
  {
    "path": "command/install_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"fmt\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/config/param\"\n\t\"github.com/Netflix/chaosmonkey/v2/mock\"\n\t\"github.com/pkg/errors\"\n\t\"io/ioutil\"\n\t\"testing\"\n)\n\nfunc assertHasSameContent(fileName string, expectedContent string) error {\n\n\tcronContent, err := ioutil.ReadFile(fileName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tactualContent := string(cronContent)\n\tif actualContent != expectedContent {\n\t\treturn errors.Errorf(\"\\nFile : %s\\nExpected:\\n%s\\nActual:\\n%s\", fileName, expectedContent, actualContent)\n\t}\n\treturn nil\n}\n\nfunc initInstallationConfig(script string, cron string, log string, term string) (*config.Monkey, error) {\n\tdefaultConfig := config.Defaults()\n\tdefaultConfig.Set(param.SchedulePath, script)\n\tdefaultConfig.Set(param.ScheduleCronPath, cron)\n\tdefaultConfig.Set(param.LogPath, log)\n\tdefaultConfig.Set(param.StartHour, 9)\n\tdefaultConfig.Set(param.TermAccount, \"root\")\n\tdefaultConfig.Set(param.TermPath, term)\n\treturn defaultConfig, nil\n}\n\nfunc TestInstallationWithDefaultCron(t *testing.T) {\n\tscriptPath := \"/tmp/chaosmonkey-schedule.sh\"\n\ttermPath := \"/tmp/chaosmonkey-terminate.sh\"\n\tcronPath := \"/tmp/chaosmonkey-schedule\"\n\texecPath := \"/tmp/chaosmonkey\"\n\tlogPath := \"/var/log\"\n\n\tdefaultConfig, err := initInstallationConfig(scriptPath, cronPath, logPath, termPath)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\texecutable := mock.Executable{Path: execPath}\n\tInstallCron(defaultConfig, executable)\n\n\texpectedCron := fmt.Sprintf(\"0 7 * * 1-5 root %s\\n\", scriptPath)\n\terr = assertHasSameContent(cronPath, expectedCron)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\texpectedScript := fmt.Sprintf(`#!/bin/bash\n%s %s \"$@\" >> %s/chaosmonkey-%s.log 2>&1\n`, execPath, \"schedule\", logPath, \"schedule\")\n\terr = assertHasSameContent(scriptPath, expectedScript)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n}\n\nfunc TestInstallationWithUserDefinedCron(t *testing.T) {\n\tscriptPath := \"/tmp/chaosmonkey-schedule.sh\"\n\ttermPath := \"/tmp/chaosmonkey-terminate.sh\"\n\tcronPath := \"/tmp/chaosmonkey-schedule\"\n\texecPath := \"/tmp/chaosmonkey\"\n\tlogPath := \"/var/log\"\n\tuserDefinedCron := \"0 15 * * 1-5\"\n\n\tdefaultConfig, err := initInstallationConfig(scriptPath, cronPath, logPath, termPath)\n\tdefaultConfig.Set(param.CronExpression, userDefinedCron)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\texecutable := mock.Executable{Path: execPath}\n\tInstallCron(defaultConfig, executable)\n\n\texpectedCron := fmt.Sprintf(\"%s root %s\\n\", userDefinedCron, scriptPath)\n\terr = assertHasSameContent(cronPath, expectedCron)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\texpectedScript := fmt.Sprintf(`#!/bin/bash\n%s %s \"$@\" >> %s/chaosmonkey-%s.log 2>&1\n`, execPath, \"schedule\", logPath, \"schedule\")\n\terr = assertHasSameContent(scriptPath, expectedScript)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "command/migrate.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/v2/mysql\"\n\t\"log\"\n)\n\n// Migrate executes database migration\nfunc Migrate(db mysql.MySQL) {\n\terr := mysql.Migrate(db)\n\n\tif err != nil {\n\t\tlog.Fatalf(\"ERROR - couldn't apply database migration: %v\", err)\n\t}\n\tlog.Println(\"database migration applied successfully\")\n}\n"
  },
  {
    "path": "command/osutil.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"github.com/kardianos/osext\"\n\t\"os\"\n)\n\n// ChaosmonkeyExecutable is a representation of Chaosmonkey executable\ntype ChaosmonkeyExecutable struct {\n}\n\n// ExecutablePath implements command.CurrentExecutable.ExecutablePath\nfunc (e ChaosmonkeyExecutable) ExecutablePath() (string, error) {\n\treturn osext.Executable()\n}\n\n// EnsureFileAbsent ensures that a file is absent, returning an error otherwise\nfunc EnsureFileAbsent(path string) error {\n\terr := os.Remove(path)\n\n\t// If it's an IsNotExist error, we can ignore it, since it\n\t// satisfies the contract of the file being absent\n\tif os.IsNotExist(err) {\n\t\treturn nil\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "command/outage.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n)\n\n// Outage prints out \"true\" if an ongoing outage, else \"false\"\nfunc Outage(ou chaosmonkey.Outage) {\n\tdown, err := ou.Outage()\n\tif err != nil {\n\t\tfmt.Printf(\"ERROR: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\tfmt.Printf(\"%t\\n\", down)\n}\n"
  },
  {
    "path": "command/regions.go",
    "content": "// Copyright 2017 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"fmt\"\n\t\"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/spinnaker\"\n\t\"github.com/SmartThingsOSS/frigga-go\"\n\t\"os\"\n)\n\n// DumpRegions lists the regions that a cluster is in\nfunc DumpRegions(cluster, account string, spin spinnaker.Spinnaker) {\n\n\tnames, err := frigga.Parse(cluster)\n\tif err != nil {\n\t\tfmt.Printf(\"ERROR: %s\", err)\n\t\tos.Exit(1)\n\t}\n\n\tregions, err := spin.GetRegionNames(names.App, deploy.AccountName(account), deploy.ClusterName(cluster))\n\tif err != nil {\n\t\tfmt.Printf(\"ERROR: %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\tfor _, region := range regions {\n\t\tfmt.Println(region)\n\t}\n\n}\n"
  },
  {
    "path": "command/schedule.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedstore\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedule\"\n)\n\n// Schedule executes the \"schedule\" command. This defines the schedule\n// of terminations for the day and records them as cron jobs\nfunc Schedule(g chaosmonkey.AppConfigGetter, ss schedstore.SchedStore, cfg *config.Monkey, d deploy.Deployment, cons schedule.Constrainer, apps []string) {\n\n\tenabled, err := cfg.ScheduleEnabled()\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: cannot determine if schedule is enabled: %v\", err)\n\t}\n\tif !enabled {\n\t\tlog.Println(\"schedule disabled, not running\")\n\t\treturn\n\t}\n\n\t/*\n\t Note: We don't check for the enable flag during scheduling, only\n\t during terminations. That way, if chaos monkey is disabled during\n\t scheduling time but later in the day becomes enabled, it still\n\t functions correctly.\n\t*/\n\terr = do(d, g, ss, cfg, cons, apps)\n\n\tif err != nil {\n\t\tlog.Fatalf(\"FATAL: %v\", err)\n\t}\n\n}\n\n// do is the actual implementation for the Schedule function\nfunc do(d deploy.Deployment, g chaosmonkey.AppConfigGetter, ss schedstore.SchedStore, cfg *config.Monkey, cons schedule.Constrainer, apps []string) error {\n\n\ts := schedule.New()\n\terr := s.Populate(d, g, cfg, apps)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to populate schedule: %v\", err)\n\t}\n\n\t// Filter out terminations that violate constrains\n\tsched := cons.Filter(*s)\n\n\terr = deploySchedule(&sched, ss, cfg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to deploy schedule: %v\", err)\n\t}\n\n\treturn nil\n}\n\n// deploySchedule publishes the schedule to chaosmonkey-api\n// and registers the schedule with the local cron\nfunc deploySchedule(s *schedule.Schedule, ss schedstore.SchedStore, cfg *config.Monkey) error {\n\tloc, err := cfg.Location()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"deploySchedule: could not retrieve local timezone: %v\", err)\n\t}\n\n\ttoday := time.Now().In(loc)\n\n\terr = ss.Publish(today, s)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"deploySchedule: could not publish schedule: %v\", err)\n\t}\n\n\terr = registerWithCron(s, cfg)\n\treturn err\n}\n\n// registerWithCron registers the schedule of terminations with cron on the local machine\n//\n// Creates or overwrites the file specified by config.Chaos.CronPath()\nfunc registerWithCron(s *schedule.Schedule, cfg *config.Monkey) error {\n\tcrontab := s.Crontab(cfg.TermPath(), cfg.TermAccount())\n\tvar perms os.FileMode = 0644 // -rw-r--r--\n\tlog.Printf(\"Writing %s\\n\", cfg.CronPath())\n\terr := ioutil.WriteFile(cfg.CronPath(), crontab, perms)\n\treturn err\n}\n"
  },
  {
    "path": "command/schedule_int_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"bytes\"\n\t\"io/ioutil\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/config/param\"\n\t\"github.com/Netflix/chaosmonkey/v2/constrainer\"\n\t\"github.com/Netflix/chaosmonkey/v2/mock\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedule\"\n)\n\n// TestSchedule verifies the schedule command generates a cron file with\n// the appropriate number of entries\nfunc TestScheduleCommand(t *testing.T) {\n\n\t// Setup\n\tcronFile := \"/tmp/chaoscron\"\n\terr := EnsureFileAbsent(cronFile)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\td := mock.Dep() // mock that returns four apps\n\ta := new(mockAPI)\n\tcfg := config.Defaults()\n\tcfg.Set(param.Enabled, true)\n\tcfg.Set(param.CronPath, cronFile)\n\tcfg.Set(param.Accounts, []string{\"prod\", \"test\"})\n\n\t// Code under test\n\tappNames, err := d.AppNames()\n\tif err != nil {\n\t\tt.Fatalf(\"%v\", err)\n\t}\n\n\terr = do(d, a, a, cfg, constrainer.NullConstrainer{}, appNames)\n\n\tif err != nil {\n\t\tt.Errorf(\"%v\", err)\n\t}\n\n\t// Assertions\n\texpectedCount := 4\n\n\tcronFileContents, err := ioutil.ReadFile(cronFile)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tactualCount := countEntries(cronFileContents)\n\n\tif actualCount != expectedCount {\n\t\tt.Errorf(\"\\nExpected:\\n%d\\nActual:\\n%d\", expectedCount, actualCount)\n\t}\n\n}\n\n// countEntries counts the number of entries in a cron file's contents\nfunc countEntries(buf []byte) int {\n\treturn bytes.Count(buf, []byte(\"\\n\"))\n}\n\n// mockAPI acts as a fake implementation of ChaosMonkeyAPI\ntype mockAPI struct {\n}\n\n// Publish implements ChaosMonkeyAPI.Publish\nfunc (a mockAPI) Publish(date time.Time, sched *schedule.Schedule) error {\n\treturn nil\n}\n\nfunc (a mockAPI) Retrieve(date time.Time) (*schedule.Schedule, error) {\n\treturn nil, nil\n}\n\n// Get implements chaosmonkey.Getter.Get\nfunc (a mockAPI) Get(name string) (*chaosmonkey.AppConfig, error) {\n\tcfg := chaosmonkey.NewAppConfig(nil)\n\tcfg.MeanTimeBetweenKillsInWorkDays = 1\n\treturn &cfg, nil\n}\n\n// Check implements api.Checker.Check\nfunc (a mockAPI) Check(term chaosmonkey.Termination, appCfg *chaosmonkey.AppConfig, endHour int, loc *time.Location) (bool, error) {\n\treturn true, nil\n}\n"
  },
  {
    "path": "command/schedule_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"io/ioutil\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/config/param\"\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedule\"\n)\n\n// addToSchedule schedules instanceId for termination at timeString\n// where timeString is  formatted in RFC3339 format\nfunc addToSchedule(t *testing.T, sched *schedule.Schedule, timeString string, group grp.InstanceGroup) {\n\ttm, err := time.Parse(time.RFC3339, timeString)\n\tif err != nil {\n\t\tt.Fatal(\"Could not parse time string:\", tm, err.Error())\n\t}\n\n\tsched.Add(tm, group)\n}\n\nfunc newClusterGroup(app, account, cluster, region string) grp.InstanceGroup {\n\treturn grp.New(app, account, region, \"\", cluster)\n}\n\nfunc TestRegisterWithCron(t *testing.T) {\n\n\t// setup\n\n\t// Ensure the file isn't there from a previous run\n\tfname := \"/tmp/chaoscron\"\n\terr := EnsureFileAbsent(fname)\n\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tconfig := config.Defaults()\n\tconfig.Set(param.Enabled, true)\n\tconfig.Set(param.CronPath, fname)\n\tconfig.Set(param.Accounts, []string{\"prod\"})\n\n\tsched := schedule.New()\n\n\t// Thu Oct 1, 2015 10:15 AM PDT -> 17:15 UTC (7 hours)\n\taddToSchedule(t, sched, \"2015-10-01T10:15:00-07:00\", newClusterGroup(\"abc\", \"prod\", \"abc-prod\", \"us-east-1\"))\n\n\t// Thu Oct 1, 2015 11:23 AM PDT -> 18:23 UTC (7 hours)\n\taddToSchedule(t, sched, \"2015-10-01T11:23:00-07:00\", newClusterGroup(\"abc\", \"prod\", \"abc-prod\", \"us-west-2\"))\n\n\t// code under test\n\terr = registerWithCron(sched, config)\n\n\tif err != nil {\n\t\tt.Fatal(err.Error())\n\t}\n\n\t// assertions\n\tdat, err := ioutil.ReadFile(fname)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tactual := string(dat)\n\texpected := `15 17 1 10 4 root /apps/chaosmonkey/chaosmonkey-terminate.sh abc prod --cluster=abc-prod --region=us-east-1\n23 18 1 10 4 root /apps/chaosmonkey/chaosmonkey-terminate.sh abc prod --cluster=abc-prod --region=us-west-2\n`\n\tif actual != expected {\n\t\tt.Errorf(\"\\nExpected:\\n%s\\nActual:\\n%s\", expected, actual)\n\t}\n}\n\n// same as TestRegisterWithCron, but reverses the order that\n// things are added to schedule.\nfunc TestCronOutputInSortedOrder(t *testing.T) {\n\t// setup\n\n\t// Ensure the file isn't there from a previous run\n\tfname := \"/tmp/chaoscron\"\n\terr := EnsureFileAbsent(fname)\n\n\tif err != nil {\n\t\tt.Fatal(err.Error())\n\t}\n\n\tconfig := config.Defaults()\n\tconfig.Set(param.Enabled, true)\n\tconfig.Set(param.CronPath, fname)\n\tconfig.Set(param.Accounts, []string{\"prod\"})\n\n\tschedule := schedule.New()\n\n\t// Thu Oct 1, 2015 11:23 AM PDT -> 18:23 UTC (7 hours)\n\taddToSchedule(t, schedule, \"2015-10-01T11:23:00-07:00\", newClusterGroup(\"abc\", \"prod\", \"abc-prod\", \"us-east-1\"))\n\n\t// Thu Oct 1, 2015 10:15 AM PDT -> 17:15 UTC (7 hours)\n\taddToSchedule(t, schedule, \"2015-10-01T10:15:00-07:00\", newClusterGroup(\"abc\", \"prod\", \"abc-prod\", \"us-west-2\"))\n\n\t// code under test\n\terr = registerWithCron(schedule, config)\n\n\tif err != nil {\n\t\tt.Fatal(err.Error())\n\t}\n\n\t// assertions\n\tdat, err := ioutil.ReadFile(fname)\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\tactual := string(dat)\n\texpected := `15 17 1 10 4 root /apps/chaosmonkey/chaosmonkey-terminate.sh abc prod --cluster=abc-prod --region=us-west-2\n23 18 1 10 4 root /apps/chaosmonkey/chaosmonkey-terminate.sh abc prod --cluster=abc-prod --region=us-east-1\n`\n\tif actual != expected {\n\t\tt.Errorf(\"\\nExpected:\\n%s\\nActual:\\n%s\", expected, actual)\n\t}\n}\n"
  },
  {
    "path": "command/terminate.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage command\n\nimport (\n\t\"log\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n\t\"github.com/Netflix/chaosmonkey/v2/term\"\n)\n\n// Terminate executes the \"terminate\" command. This selects an instance\n// based on the app, account, region, stack, cluster passed\n//\n// region, stack, and cluster may be blank\nfunc Terminate(d deps.Deps, app string, account string, region string, stack string, cluster string) {\n\terr := term.Terminate(d, app, account, region, stack, cluster)\n\tif err != nil {\n\t\tcerr := d.ErrCounter.Increment()\n\t\tif cerr != nil {\n\t\t\tlog.Printf(\"WARNING could not increment error counter: %v\", cerr)\n\t\t}\n\t\tlog.Fatalf(\"FATAL %v\\n\\nstack trace:\\n%+v\", err, err)\n\t}\n}\n"
  },
  {
    "path": "config/config.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package config exposes configuration information\npackage config\n"
  },
  {
    "path": "config/config_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport \"testing\"\n\nfunc TestGetStringSlice(t *testing.T) {\n\tcfg := Defaults()\n\tcfg.Set(\"myparam1\", `[\"foo\", \"bar\"]`)\n\tcfg.Set(\"myparam2\", []string{\"foo\", \"bar\"})\n\tcfg.Set(\"myparam3\", []interface{}{interface{}(\"foo\"), interface{}(\"bar\")})\n\n\tfor _, param := range []string{\"myparam1\", \"myparam2\", \"myparam3\"} {\n\t\tgot, err := cfg.getStringSlice(param)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\n\t\tif len(got) != 2 || got[0] != \"foo\" || got[1] != \"bar\" {\n\t\t\tt.Errorf(`param %s, got %+v want [\"foo\", \"bar\"]`, param, got)\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "config/monkey.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"math\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/spf13/pflag\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/config/param\"\n)\n\n// Monkey is is a config implementation backed by viper\ntype Monkey struct {\n\tremote bool // if true, there's a remote provider\n\tv      *viper.Viper\n}\n\nconst (\n\tclockStartHour      int = 0\n\tclockEndHour        int = 23\n\thoursInClock        int = 24\n\tcronBeforeStartHour int = 2\n)\n\nfunc (m *Monkey) setDefaults() {\n\tm.v.SetDefault(param.Enabled, false)\n\tm.v.SetDefault(param.Leashed, true)\n\tm.v.SetDefault(param.ScheduleEnabled, false)\n\tm.v.SetDefault(param.Accounts, []string{})\n\tm.v.SetDefault(param.StartHour, 9)\n\tm.v.SetDefault(param.EndHour, 15)\n\tm.v.SetDefault(param.TimeZone, \"America/Los_Angeles\")\n\tm.v.SetDefault(param.CronPath, \"/etc/cron.d/chaosmonkey-daily-terminations\")\n\tm.v.SetDefault(param.TermPath, \"/apps/chaosmonkey/chaosmonkey-terminate.sh\")\n\tm.v.SetDefault(param.TermAccount, \"root\")\n\tm.v.SetDefault(param.MaxApps, math.MaxInt32)\n\tm.v.SetDefault(param.Trackers, []string{})\n\tm.v.SetDefault(param.Decryptor, \"\")\n\tm.v.SetDefault(param.OutageChecker, \"\")\n\n\tm.v.SetDefault(param.DatabasePort, 3306)\n\n\tm.v.SetDefault(param.SpinnakerEndpoint, \"\")\n\tm.v.SetDefault(param.SpinnakerCertificate, \"\")\n\tm.v.SetDefault(param.SpinnakerEncryptedPassword, \"\")\n\tm.v.SetDefault(param.SpinnakerUser, \"\")\n\tm.v.SetDefault(param.SpinnakerX509Cert, \"\")\n\tm.v.SetDefault(param.SpinnakerX509Key, \"\")\n\n\tm.v.SetDefault(param.DynamicProvider, \"\")\n\tm.v.SetDefault(param.DynamicEndpoint, \"\")\n\tm.v.SetDefault(param.DynamicPath, \"\")\n\n\tm.v.SetDefault(param.ScheduleCronPath, \"/etc/cron.d/chaosmonkey-schedule\")\n\tm.v.SetDefault(param.SchedulePath, \"/apps/chaosmonkey/chaosmonkey-schedule.sh\")\n\tm.v.SetDefault(param.LogPath, \"/var/log\")\n}\n\nfunc (m *Monkey) setupEnvVarReader() {\n\t// read from environment variables\n\tm.v.AutomaticEnv()\n\n\t// Replace \".\" with \"_\" when reading environment variables\n\t// e.g.: chaosmonkey.enabled -> CHAOSMONKEY_ENABLED\n\tm.v.SetEnvKeyReplacer(strings.NewReplacer(\".\", \"_\"))\n}\n\n// Load returns a Monkey config that loads config from a file\nfunc Load(configPaths []string) (*Monkey, error) {\n\tm := &Monkey{v: viper.New()}\n\n\tm.setDefaults()\n\tm.setupEnvVarReader()\n\n\tfor _, dir := range configPaths {\n\t\tm.v.AddConfigPath(dir)\n\t}\n\n\tm.v.SetConfigType(\"toml\")\n\tm.v.SetConfigName(\"chaosmonkey\")\n\n\terr := m.v.ReadInConfig()\n\t// It's ok if the config file doesn't exist, but we want to catch any\n\t// other config-related issues\n\tif err != nil {\n\t\tif !os.IsNotExist(err) {\n\t\t\treturn nil, errors.Wrapf(err, \"failed to read config file\")\n\t\t}\n\n\t\tlog.Printf(\"no config file found, proceeding without one\")\n\t}\n\n\terr = m.configureRemote()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn m, nil\n}\n\n// Defaults returns a Monkey config that just has the default values set\n// it will not load local files or remote ones\nfunc Defaults() *Monkey {\n\tv := &Monkey{v: viper.New()}\n\tv.setDefaults()\n\treturn v\n}\n\n// NewFromReader returns a Monkey config which parses the initial config\n// from a reader. It may load remote if configured to\n// Config file must be in toml format\nfunc NewFromReader(in io.Reader) (*Monkey, error) {\n\tm := &Monkey{v: viper.New()}\n\tm.setDefaults()\n\tm.v.SetConfigType(\"toml\")\n\terr := m.v.ReadConfig(in)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to parse config\")\n\t}\n\n\terr = m.configureRemote()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn m, nil\n\n}\n\n// configureRemote configures viper for a remote provider if the user has\n// specified one\nfunc (m *Monkey) configureRemote() error {\n\tprovider := m.v.GetString(param.DynamicProvider)\n\tendpoint := m.v.GetString(param.DynamicEndpoint)\n\tpath := m.v.GetString(param.DynamicPath)\n\n\t// If the user specified an external provider, use it\n\tif provider != \"\" {\n\t\tm.remote = true\n\t\tm.v.SetConfigType(\"json\")\n\t\terr := m.v.AddRemoteProvider(provider, endpoint, path)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"failed viper.AddRemoteProvider(provider=\\\"%s\\\", endpoint=\\\"%s\\\", path=\\\"%s\\\"):\", provider, endpoint, path)\n\t\t}\n\t}\n\treturn nil\n}\n\n// SetRemoteProvider sets remote configuration parameters.\n// These will typically be set by parsing the config files. This method\n// exists to facilitate testing\nfunc (m *Monkey) SetRemoteProvider(provider string, endpoint string, path string) error {\n\tm.v.Set(param.DynamicProvider, provider)\n\tm.v.Set(param.DynamicEndpoint, endpoint)\n\tm.v.Set(param.DynamicPath, path)\n\n\treturn m.configureRemote()\n}\n\n// Set overrides the config value. Used for testing\nfunc (m *Monkey) Set(key string, value interface{}) {\n\tm.v.Set(key, value)\n}\n\n// readRemoteConfig retrieves config parameters from a remote source\n// If no remote source has been configured, this is a no-op\nfunc (m *Monkey) readRemoteConfig() error {\n\tif !m.remote {\n\t\treturn nil\n\t}\n\treturn m.v.ReadRemoteConfig()\n}\n\n// Enabled returns true if Chaos Monkey is enabled\nfunc (m *Monkey) Enabled() (bool, error) {\n\treturn m.getDynamicBool(param.Enabled)\n}\n\n// Leashed returns true if Chaos Monkey is leashed\n// In leashed mode, Chaos Monkey records terminations but does not actually\n// terminate\nfunc (m *Monkey) Leashed() (bool, error) {\n\treturn m.getDynamicBool(param.Leashed)\n}\n\n// ScheduleEnabled returns true if Chaos Monkey termination scheduling is enabled\n// if false, Chaos Monkey will not generate a termination schedule\nfunc (m *Monkey) ScheduleEnabled() (bool, error) {\n\treturn m.getDynamicBool(param.ScheduleEnabled)\n}\n\nfunc (m *Monkey) getDynamicBool(param string) (bool, error) {\n\terr := m.readRemoteConfig()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn m.v.GetBool(param), nil\n}\n\n// AccountEnabled returns true if Chaos Monkey is enabled for that account\nfunc (m *Monkey) AccountEnabled(account string) (bool, error) {\n\taccounts, err := m.Accounts()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tfor _, x := range accounts {\n\t\tif account == x {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\n// Accounts return a list of accounts where Choas Monkey is enabled\nfunc (m *Monkey) Accounts() ([]string, error) {\n\terr := m.readRemoteConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn m.getStringSlice(param.Accounts)\n}\n\n// toStrings converts a slice of interfaces to a slice of strings\nfunc toStrings(values []interface{}) ([]string, error) {\n\tresult := make([]string, len(values))\n\tfor i, x := range values {\n\t\tx, valid := x.(string)\n\t\tif !valid {\n\t\t\treturn nil, errors.Errorf(\"non-string in %v\", values)\n\t\t}\n\t\tresult[i] = x\n\t}\n\treturn result, nil\n}\n\n// StartHour (o'clock) is when Chaos\n// Monkey starts terminating this value is in [0,23] This is time-zone\n// dependent, see the Location method\nfunc (m *Monkey) StartHour() int { return m.v.GetInt(param.StartHour) }\n\n// EndHour (o'clock) is the time after which Chaos Monkey will\n// not terminate instances.\n// this value is in [0,23]\n// This is time-zone dependent, see the Location method\nfunc (m *Monkey) EndHour() int {\n\treturn m.v.GetInt(param.EndHour)\n}\n\n// Location returns the time zone of StartHour and EndHour.\n// May return an error if time.LoadLocation fails\nfunc (m *Monkey) Location() (*time.Location, error) {\n\treturn time.LoadLocation(m.v.GetString(param.TimeZone))\n}\n\n// CronPath returns the path to where Chaos Monkey\n// puts the cron job file with daily terminations\nfunc (m *Monkey) CronPath() string {\n\treturn m.v.GetString(param.CronPath)\n}\n\n// TermPath returns the path to the executable that\n// wraps the chaos monkey binary for terminating instances\nfunc (m *Monkey) TermPath() string {\n\treturn m.v.GetString(param.TermPath)\n}\n\n// TermAccount returns the account that cron will use\n// to execute the termination command\nfunc (m *Monkey) TermAccount() string {\n\treturn m.v.GetString(param.TermAccount)\n}\n\n// MaxApps returns the maximum number of apps to\n// examine for termination\nfunc (m *Monkey) MaxApps() int {\n\treturn m.v.GetInt(param.MaxApps)\n}\n\n// Trackers returns the names of the backend implementation for\n// termination trackers. Used for things like logging and metrics collection\nfunc (m *Monkey) Trackers() ([]string, error) {\n\treturn m.getStringSlice(param.Trackers)\n}\n\n// ErrorCounter returns the names of the backend implementions for\n// error counters. Intended for monitoring/alerting.\nfunc (m *Monkey) ErrorCounter() string {\n\treturn m.v.GetString(param.ErrorCounter)\n}\n\nfunc (m *Monkey) getStringSlice(key string) ([]string, error) {\n\t// This could be encoded natively as a list of strings, or as a string that\n\t// represents a list of strings, so we need to handle both cases\n\tt := m.v.Get(key)\n\tif t == nil {\n\t\treturn nil, fmt.Errorf(\"%s not specified\", param.Accounts)\n\t}\n\n\tswitch t := t.(type) {\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"%s: unexpected type %T\", param.Accounts, t)\n\tcase []string: // When set explicitly in code\n\t\treturn t, nil\n\tcase []interface{}: // When reading from config file\n\t\treturn toStrings(t)\n\tcase string: // When reading from prana, which uses string encoding\n\t\t// Convert to list of strings\n\t\tvar result []string\n\t\terr := json.Unmarshal([]byte(t), &result)\n\t\treturn result, err\n\t}\n}\n\n// SpinnakerEndpoint returns the spinnaker endpoint\nfunc (m *Monkey) SpinnakerEndpoint() string {\n\treturn m.v.GetString(param.SpinnakerEndpoint)\n}\n\n// SpinnakerCertificate retunrs a path to a .p12 file that contains a TLS cert\n// for authenticating against Spinnaker\nfunc (m *Monkey) SpinnakerCertificate() string {\n\treturn m.v.GetString(param.SpinnakerCertificate)\n}\n\n// SpinnakerEncryptedPassword returns an password that\n// is used to decrypt the Spinnaker certificate. The encryption scheme\n// is defined by the Decryptor parameter\nfunc (m *Monkey) SpinnakerEncryptedPassword() string {\n\treturn m.v.GetString(param.SpinnakerEncryptedPassword)\n}\n\n// SpinnakerUser is sent in the \"user\" field in the terminateInstances task sent\n// to Spinnaker when Spinnaker terminates an instance\nfunc (m *Monkey) SpinnakerUser() string {\n\treturn m.v.GetString(param.SpinnakerUser)\n}\n\n// SpinnakerX509Cert retunrs a path to a X509 cert file\nfunc (m *Monkey) SpinnakerX509Cert() string {\n\treturn m.v.GetString(param.SpinnakerX509Cert)\n}\n\n// SpinnakerX509Key retunrs a path to a X509 key file\nfunc (m *Monkey) SpinnakerX509Key() string {\n\treturn m.v.GetString(param.SpinnakerX509Key)\n}\n\n// Decryptor returns an interface for decrypting secrets\nfunc (m *Monkey) Decryptor() string {\n\treturn m.v.GetString(param.Decryptor)\n}\n\n// OutageChecker returns an interface for checking if there is an ongoing\n// outage\nfunc (m *Monkey) OutageChecker() string {\n\treturn m.v.GetString(param.OutageChecker)\n}\n\n// DatabaseHost returns the hostname the database is running on\nfunc (m *Monkey) DatabaseHost() string {\n\treturn m.v.GetString(param.DatabaseHost)\n}\n\n// DatabasePort returns the port the database is listening on\nfunc (m *Monkey) DatabasePort() int {\n\treturn m.v.GetInt(param.DatabasePort)\n}\n\n// DatabaseUser returns the database user associated with the credentials\nfunc (m *Monkey) DatabaseUser() string {\n\treturn m.v.GetString(param.DatabaseUser)\n}\n\n// DatabaseName returns the name of the database that stores the Chaos Monkey\n// state\nfunc (m *Monkey) DatabaseName() string {\n\treturn m.v.GetString(param.DatabaseName)\n}\n\n// DatabaseEncryptedPassword returns an encrypted version of the database\n// credentials\nfunc (m *Monkey) DatabaseEncryptedPassword() string {\n\treturn m.v.GetString(param.DatabaseEncryptedPassword)\n}\n\n// BindPFlag binds a specific parameter to a pflag\nfunc (m *Monkey) BindPFlag(parameter string, flag *pflag.Flag) (err error) {\n\treturn m.v.BindPFlag(parameter, flag)\n}\n\n// The code below is to provide a mechanism for adding a new remote config\n// provider without directly viper. Viper wasn't designed for this use-case\n// so this is a workaround.\n\n// RemoteProvider is a type alias\ntype RemoteProvider viper.RemoteProvider\n\n// RemoteConfigFactory is the same interface as viper.remoteConfigFactory\n// This is a workaround to be able to support backends other than etc/consul\n// without modifying viper\ntype RemoteConfigFactory interface {\n\tGet(rp RemoteProvider) (io.Reader, error)\n\tWatch(rp RemoteProvider) (io.Reader, error)\n}\n\ntype proxy struct {\n\tfactory RemoteConfigFactory\n}\n\nfunc (p proxy) Get(rp viper.RemoteProvider) (io.Reader, error) {\n\treturn p.factory.Get(rp)\n}\n\nfunc (p proxy) Watch(rp viper.RemoteProvider) (io.Reader, error) {\n\treturn p.factory.Watch(rp)\n}\n\n// SetRemoteProvider sets viper's remote provider\nfunc SetRemoteProvider(name string, factory RemoteConfigFactory) {\n\tviper.RemoteConfig = proxy{factory}\n\tviper.SupportedRemoteProviders = []string{name}\n}\n\n// CronExpression returns the chaosmonkey main run cron expression.\n// It defaults to 2 hour before start_hour on weekdays, if no cron expression\n// is specified in the config\nfunc (m *Monkey) CronExpression() (string, error) {\n\tdefaultCron := \"0 %d * * 1-5\"\n\tcron := m.v.Get(param.CronExpression)\n\tif cron == nil {\n\t\trunAtHour, err := calculateDefaultCronRunHour(m.StartHour())\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn fmt.Sprintf(defaultCron, runAtHour), nil\n\t}\n\tswitch cron := cron.(type) {\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"%s: unexpected type %T\", param.CronExpression, cron)\n\tcase string:\n\t\treturn cron, nil\n\t}\n}\n\n// calculates the default cron run hour based on startHour.\n// The default cron starts \"cronBeforeStartHour\" hours\n// before \"startHour\"\nfunc calculateDefaultCronRunHour(startHour int) (int, error) {\n\tif (startHour < clockStartHour) || (startHour > clockEndHour) {\n\t\treturn -1, errors.Errorf(\"%d is not in cron range(0-23)\", startHour)\n\t}\n\trunAtHour := startHour - cronBeforeStartHour\n\tif runAtHour < 0 {\n\t\t// assuming a 24 hour clock system(0 - 23), -ve values means going back to previous day\n\t\t// e.g. if start hour is 0 (midnight), the \"cronTime\" time should be 22 hours\n\t\t// on the previous day.\n\t\treturn hoursInClock + runAtHour, nil\n\t}\n\treturn runAtHour, nil\n}\n\n// ScheduleCronPath returns the path to which\n// main chaosmonkey crontab is located\nfunc (m *Monkey) ScheduleCronPath() string {\n\treturn m.v.GetString(param.ScheduleCronPath)\n}\n\n// SchedulePath returns the path to which main\n// chaosmonkey schedule script(invoked from cron) is located\nfunc (m *Monkey) SchedulePath() string {\n\treturn m.v.GetString(param.SchedulePath)\n}\n\n// LogPath returns the path to which\n// log files should be written\nfunc (m *Monkey) LogPath() string {\n\treturn m.v.GetString(param.LogPath)\n}\n"
  },
  {
    "path": "config/monkey_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"fmt\"\n\t\"github.com/Netflix/chaosmonkey/v2/config/param\"\n\t\"testing\"\n)\n\nfunc TestDefaultCron(t *testing.T) {\n\tmonkey := Defaults()\n\tmonkey.Set(param.StartHour, 9)\n\n\tactual, err := monkey.CronExpression()\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\texpected := fmt.Sprintf(\"0 %d * * 1-5\", 7)\n\tif actual != expected {\n\t\tt.Errorf(\"\\nExpected:\\n%s\\nActual:\\n%s\", expected, actual)\n\t}\n}\n\nfunc TestDefaultCronForStartHourMidnight(t *testing.T) {\n\tmonkey := Defaults()\n\tmonkey.Set(param.StartHour, 0)\n\n\tactual, err := monkey.CronExpression()\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\texpected := fmt.Sprintf(\"0 %d * * 1-5\", 22)\n\tif actual != expected {\n\t\tt.Errorf(\"\\nExpected:\\n%s\\nActual:\\n%s\", expected, actual)\n\t}\n}\n\nfunc TestDefaultCronForStartHourOneAM(t *testing.T) {\n\tmonkey := Defaults()\n\tmonkey.Set(param.StartHour, 1)\n\n\tactual, err := monkey.CronExpression()\n\tif err != nil {\n\t\tt.Error(err.Error())\n\t\treturn\n\t}\n\n\texpected := fmt.Sprintf(\"0 %d * * 1-5\", 23)\n\tif actual != expected {\n\t\tt.Errorf(\"\\nExpected:\\n%s\\nActual:\\n%s\", expected, actual)\n\t}\n}\n\nfunc TestDefaultCronForStartHourBeforeClockStart(t *testing.T) {\n\tmonkey := Defaults()\n\tmonkey.Set(param.StartHour, -1)\n\n\t_, err := monkey.CronExpression()\n\tif err == nil {\n\t\tt.Error(\"Expected InstalledCronExpression to return an error as start hour is before clock start hour\")\n\t\treturn\n\t}\n}\n\nfunc TestDefaultCronForStartHourAfterClockEnd(t *testing.T) {\n\tmonkey := Defaults()\n\tmonkey.Set(param.StartHour, 24)\n\n\t_, err := monkey.CronExpression()\n\tif err == nil {\n\t\tt.Error(\"Expected InstalledCronExpression to return an error as start hour is after clock end hour\")\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "config/param/param.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage param\n\n// properties\nconst (\n\tEnabled          = \"chaosmonkey.enabled\"\n\tLeashed          = \"chaosmonkey.leashed\"\n\tScheduleEnabled  = \"chaosmonkey.schedule_enabled\"\n\tAccounts         = \"chaosmonkey.accounts\"\n\tStartHour        = \"chaosmonkey.start_hour\"\n\tEndHour          = \"chaosmonkey.end_hour\"\n\tTimeZone         = \"chaosmonkey.time_zone\"\n\tCronPath         = \"chaosmonkey.cron_path\"\n\tTermPath         = \"chaosmonkey.term_path\"\n\tTermAccount      = \"chaosmonkey.term_account\"\n\tMaxApps          = \"chaosmonkey.max_apps\"\n\tTrackers         = \"chaosmonkey.trackers\"\n\tErrorCounter     = \"chaosmonkey.error_counter\"\n\tDecryptor        = \"chaosmonkey.decryptor\"\n\tOutageChecker    = \"chaosmonkey.outage_checker\"\n\tCronExpression   = \"chaosmonkey.cron_expression\"\n\tScheduleCronPath = \"chaosmonkey.schedule_cron_path\"\n\tSchedulePath     = \"chaosmonkey.schedule_path\"\n\tLogPath          = \"chaosmonkey.log_path\"\n\n\t// spinnaker\n\tSpinnakerEndpoint          = \"spinnaker.endpoint\"\n\tSpinnakerCertificate       = \"spinnaker.certificate\"\n\tSpinnakerEncryptedPassword = \"spinnaker.encrypted_password\"\n\tSpinnakerUser              = \"spinnaker.user\"\n\tSpinnakerX509Cert          = \"spinnaker.x509_cert\"\n\tSpinnakerX509Key           = \"spinnaker.x509_key\"\n\t// database\n\tDatabaseHost              = \"database.host\"\n\tDatabasePort              = \"database.port\"\n\tDatabaseUser              = \"database.user\"\n\tDatabaseEncryptedPassword = \"database.encrypted_password\"\n\tDatabaseName              = \"database.name\"\n\n\t// dynamic property provider\n\tDynamicProvider = \"dynamic.provider\"\n\tDynamicEndpoint = \"dynamic.endpoint\"\n\tDynamicPath     = \"dynamic.path\"\n)\n"
  },
  {
    "path": "constrainer/constrainer.go",
    "content": "// Copyright 2017 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage constrainer\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedule\"\n)\n\n// NullConstrainer is a no-op constrainer\ntype NullConstrainer struct{}\n\nfunc init() {\n\tdeps.GetConstrainer = getNullConstrainer\n}\n\n// Filter implements schedule.Constrainer.Filter\n// This is a no-op implementation\nfunc (n NullConstrainer) Filter(s schedule.Schedule) schedule.Schedule {\n\treturn s\n}\n\nfunc getNullConstrainer(cfg *config.Monkey) (schedule.Constrainer, error) {\n\treturn NullConstrainer{}, nil\n}\n"
  },
  {
    "path": "decryptor/decryptor.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage decryptor\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n\t\"github.com/pkg/errors\"\n)\n\ntype nullDecryptor struct{}\n\n// Decrypt implements chaosmonkey.Decryptor.Decrypt\n// This is a no-op implementation that simply returns the plaintext\nfunc (n nullDecryptor) Decrypt(ciphertext string) (string, error) {\n\treturn ciphertext, nil\n}\n\nfunc init() {\n\tdeps.GetDecryptor = getNullDecryptor\n}\n\nfunc getNullDecryptor(cfg *config.Monkey) (chaosmonkey.Decryptor, error) {\n\tkind := cfg.Decryptor()\n\tif kind != \"\" {\n\t\treturn nil, errors.Errorf(\"unsupported decryptor: %s\", kind)\n\t}\n\n\treturn nullDecryptor{}, nil\n}\n"
  },
  {
    "path": "deploy/app.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage deploy\n\n// App represents an application\ntype App struct {\n\tname     string\n\taccounts []*Account\n}\n\n// Name returns the name of an app\nfunc (a App) Name() string {\n\treturn a.name\n}\n\n// Accounts returns a slice of accounts\nfunc (a App) Accounts() []*Account {\n\treturn a.accounts\n}\n\ntype (\n\t// AppName is the name of an app\n\tAppName string\n\n\t// AccountName is the name of a cloud account\n\tAccountName string\n\n\t// ClusterName is the app-stack-detail name of a cluster\n\tClusterName string\n\n\t// StackName is the stack part of the cluster name\n\tStackName string\n\n\t// RegionName is the name of an AWS region\n\tRegionName string\n\n\t// ASGName is the app-stack-detail-sequence name of an ASG\n\tASGName string\n\n\t// InstanceID is the i-xxxxxx name of an AWS instance or uuid of a container\n\tInstanceID string\n\n\t// CloudProvider is the name of the cloud backend (e.g., aws)\n\tCloudProvider string\n\n\t// ClusterMap maps cluster name to information about instances by region and\n\t// ASG\n\tClusterMap map[ClusterName]map[RegionName]map[ASGName][]InstanceID\n\n\t// AccountInfo tracks the provider and the clusters\n\tAccountInfo struct {\n\t\tCloudProvider string\n\t\tClusters      ClusterMap\n\t}\n\n\t// AppMap is a map that tracks info about an app\n\tAppMap map[AccountName]AccountInfo\n)\n\n// NewApp constructs a new App\nfunc NewApp(name string, data AppMap) *App {\n\tapp := App{name: name}\n\tfor accountName, accountInfo := range data {\n\t\taccount := Account{name: string(accountName), app: &app, cloudProvider: accountInfo.CloudProvider}\n\t\tapp.accounts = append(app.accounts, &account)\n\t\tfor clusterName, clusterValue := range accountInfo.Clusters {\n\t\t\tcluster := Cluster{name: string(clusterName), account: &account}\n\t\t\taccount.clusters = append(account.clusters, &cluster)\n\t\t\tfor regionName, regionValue := range clusterValue {\n\t\t\t\tfor asgName, instanceIds := range regionValue {\n\t\t\t\t\tasg := ASG{\n\t\t\t\t\t\tname:    string(asgName),\n\t\t\t\t\t\tregion:  string(regionName),\n\t\t\t\t\t\tcluster: &cluster,\n\t\t\t\t\t}\n\t\t\t\t\tcluster.asgs = append(cluster.asgs, &asg)\n\t\t\t\t\tfor _, id := range instanceIds {\n\t\t\t\t\t\tinstance := Instance{\n\t\t\t\t\t\t\tid:  string(id),\n\t\t\t\t\t\t\tasg: &asg,\n\t\t\t\t\t\t}\n\t\t\t\t\t\tasg.instances = append(asg.instances, &instance)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &app\n}\n"
  },
  {
    "path": "deploy/asg.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage deploy\n\nimport frigga \"github.com/SmartThingsOSS/frigga-go\"\n\n// ASG identifies an autoscaling group in the deployment\ntype ASG struct {\n\tname      string\n\tregion    string\n\tinstances []*Instance\n\tcluster   *Cluster\n}\n\n// NewASG creates a new ASG\nfunc NewASG(name, region string, instanceIDs []string, cluster *Cluster) *ASG {\n\tresult := ASG{\n\t\tname:      name,\n\t\tregion:    region,\n\t\tinstances: make([]*Instance, len(instanceIDs)),\n\t\tcluster:   cluster,\n\t}\n\n\tfor i, id := range instanceIDs {\n\t\tresult.instances[i] = &Instance{id, &result}\n\t}\n\n\treturn &result\n}\n\n// Instances returns a slice of the instances associated with the ASG\nfunc (a *ASG) Instances() []*Instance {\n\treturn a.instances\n}\n\n// Empty returns true if the ASG does not contain any instances\nfunc (a *ASG) Empty() bool {\n\treturn len(a.instances) == 0\n}\n\n// AppName returns the name of the app associated with this ASG\nfunc (a *ASG) AppName() string {\n\treturn a.cluster.AppName()\n}\n\n// AccountName returns the name of the AWS account associated with the ASG\nfunc (a *ASG) AccountName() string {\n\treturn a.cluster.AccountName()\n}\n\n// ClusterName returns the name of the cluster associated with the ASG\nfunc (a *ASG) ClusterName() string {\n\treturn a.cluster.name\n}\n\n// DetailName returns the name of the detail field associated with the ASG\nfunc (a *ASG) DetailName() string {\n\n\tasgName := a.Name()\n\n\tif a.missingPushNumber() {\n\t\t/*\n\t\t\tASGs that were launched before Spinnaker existed may be missing the -vXXX\n\t\t\tpush number at the end of the ASG. If this happens, we need to guard\n\t\t\tagainst the case where the detail field happens to match the push\n\t\t\tfield syntax.\n\n\t\t\tIn this case, we work around it by appending a phony push number before\n\t\t\tparsing with frigga.\n\t\t*/\n\t\tasgName += \"-v000\"\n\t}\n\n\tnames, err := frigga.Parse(asgName)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn names.Detail\n}\n\n// missingPushNumber returns true if the ASG does not have an associated push\n// number\nfunc (a *ASG) missingPushNumber() bool {\n\treturn a.Name() == a.ClusterName()\n}\n\n// RegionName returns the name of the region associated with the ASG\nfunc (a *ASG) RegionName() string {\n\treturn a.region\n}\n\n// Name returns the name of the ASG\nfunc (a *ASG) Name() string {\n\treturn a.name\n}\n\n// StackName returns the name of the stack\nfunc (a *ASG) StackName() string {\n\treturn a.cluster.StackName()\n}\n\n// CloudProvider returns the cloud provider (e.g., \"aws\")\nfunc (a *ASG) CloudProvider() string {\n\treturn a.cluster.CloudProvider()\n}\n"
  },
  {
    "path": "deploy/deploy_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage deploy\n\nimport (\n\t\"reflect\"\n\t\"runtime\"\n\t\"testing\"\n)\n\nfunc TestASGAndClusters(t *testing.T) {\n\tnameOf := func(f interface{}) string {\n\t\treturn runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()\n\t}\n\n\ttype tcase struct {\n\t\tappName     string\n\t\taccountName string\n\t\tregionName  string\n\t\tclusterName string\n\t\tasgName     string\n\t\tids         []string\n\t}\n\n\tmakeClusterASG := func(tc tcase) (*Cluster, *ASG) {\n\t\tvar cluster Cluster\n\t\tvar account Account\n\t\tvar app App\n\n\t\tcloudProvider := \"aws\"\n\n\t\tasg := NewASG(tc.asgName, tc.regionName, tc.ids, &cluster)\n\t\tcluster = Cluster{tc.clusterName, []*ASG{asg}, &account}\n\t\taccount = Account{tc.accountName, []*Cluster{&cluster}, &app, cloudProvider}\n\t\tapp = App{tc.appName, []*Account{&account}}\n\n\t\treturn &cluster, asg\n\t}\n\n\ttype at struct {\n\t\tf    func(*ASG) string\n\t\twant string\n\t}\n\n\ttype ct struct {\n\t\tf    func(*Cluster) string\n\t\twant string\n\t}\n\n\tvar tests = []struct {\n\t\tscenario string\n\t\tt        tcase\n\t\ta        []at\n\t\tc        []ct\n\t}{\n\t\t{\n\t\t\t\"stack and detail\",\n\t\t\ttcase{\"foo\", \"test\", \"us-east-1\", \"foo-staging-bar\", \"foo-staging-bar-v031\", []string{\"i-ff075688\", \"i-d9165a77\"}},\n\t\t\t[]at{\n\t\t\t\t{(*ASG).Name, \"foo-staging-bar-v031\"},\n\t\t\t\t{(*ASG).AppName, \"foo\"},\n\t\t\t\t{(*ASG).AccountName, \"test\"},\n\t\t\t\t{(*ASG).RegionName, \"us-east-1\"},\n\t\t\t\t{(*ASG).ClusterName, \"foo-staging-bar\"},\n\t\t\t\t{(*ASG).StackName, \"staging\"},\n\t\t\t\t{(*ASG).DetailName, \"bar\"},\n\t\t\t},\n\t\t\t[]ct{\n\t\t\t\t{(*Cluster).Name, \"foo-staging-bar\"},\n\t\t\t\t{(*Cluster).AppName, \"foo\"},\n\t\t\t\t{(*Cluster).AccountName, \"test\"},\n\t\t\t\t{(*Cluster).StackName, \"staging\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"no detail\",\n\t\t\ttcase{\"chaosguineapig\", \"prod\", \"eu-west-1\", \"chaosguineapig-staging\", \"chaosguineapig-staging-v000\", []string{\"i-7f40bbf5\", \"i-7a61d6f2\"}},\n\t\t\t[]at{\n\t\t\t\t{(*ASG).Name, \"chaosguineapig-staging-v000\"},\n\t\t\t\t{(*ASG).AppName, \"chaosguineapig\"},\n\t\t\t\t{(*ASG).AccountName, \"prod\"},\n\t\t\t\t{(*ASG).RegionName, \"eu-west-1\"},\n\t\t\t\t{(*ASG).ClusterName, \"chaosguineapig-staging\"},\n\t\t\t\t{(*ASG).StackName, \"staging\"},\n\t\t\t\t{(*ASG).DetailName, \"\"},\n\t\t\t},\n\t\t\t[]ct{\n\t\t\t\t{(*Cluster).Name, \"chaosguineapig-staging\"},\n\t\t\t\t{(*Cluster).AppName, \"chaosguineapig\"},\n\t\t\t\t{(*Cluster).AccountName, \"prod\"},\n\t\t\t\t{(*Cluster).StackName, \"staging\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"no stack\",\n\t\t\ttcase{\"chaosguineapig\", \"test\", \"eu-west-1\", \"chaosguineapig\", \"chaosguineapig-v030\", []string{\"i-7f40bbf5\", \"i-7a61d6f2\"}},\n\t\t\t[]at{\n\t\t\t\t{(*ASG).Name, \"chaosguineapig-v030\"},\n\t\t\t\t{(*ASG).AppName, \"chaosguineapig\"},\n\t\t\t\t{(*ASG).AccountName, \"test\"},\n\t\t\t\t{(*ASG).RegionName, \"eu-west-1\"},\n\t\t\t\t{(*ASG).ClusterName, \"chaosguineapig\"},\n\t\t\t\t{(*ASG).StackName, \"\"},\n\t\t\t\t{(*ASG).DetailName, \"\"},\n\t\t\t},\n\t\t\t[]ct{\n\t\t\t\t{(*Cluster).Name, \"chaosguineapig\"},\n\t\t\t\t{(*Cluster).AppName, \"chaosguineapig\"},\n\t\t\t\t{(*Cluster).AccountName, \"test\"},\n\t\t\t\t{(*Cluster).StackName, \"\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// We hit one case where there was a cluster with a name like foo-bar-v2, where the\n\t\t\t// asg had the same name: foo-bar-v2. The ASG had no push number, and the\n\t\t\t// detail looks like a push number.\n\t\t\t\"detail looks like push number\",\n\t\t\ttcase{\"foo\", \"prod\", \"us-west-2\", \"foo-bar-v2\", \"foo-bar-v2\", []string{\"i-c7a513fc\", \"i-e06cfef1\"}},\n\t\t\t[]at{\n\t\t\t\t{(*ASG).Name, \"foo-bar-v2\"},\n\t\t\t\t{(*ASG).AppName, \"foo\"},\n\t\t\t\t{(*ASG).AccountName, \"prod\"},\n\t\t\t\t{(*ASG).RegionName, \"us-west-2\"},\n\t\t\t\t{(*ASG).ClusterName, \"foo-bar-v2\"},\n\t\t\t\t{(*ASG).StackName, \"bar\"},\n\t\t\t\t{(*ASG).DetailName, \"v2\"},\n\t\t\t},\n\t\t\t[]ct{\n\t\t\t\t{(*Cluster).Name, \"foo-bar-v2\"},\n\t\t\t\t{(*Cluster).AppName, \"foo\"},\n\t\t\t\t{(*Cluster).AccountName, \"prod\"},\n\t\t\t\t{(*Cluster).StackName, \"bar\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tcluster, asg := makeClusterASG(tt.t)\n\n\t\t// ASG tests\n\t\tfor _, att := range tt.a {\n\t\t\tif got, want := att.f(asg), att.want; got != want {\n\t\t\t\tt.Errorf(\"scenario %s: got %s()=%s, want: %s\", tt.scenario, nameOf(att.f), got, want)\n\t\t\t}\n\t\t}\n\n\t\t// cluster tests\n\t\tfor _, ctt := range tt.c {\n\t\t\tif got, want := ctt.f(cluster), ctt.want; got != want {\n\t\t\t\tt.Errorf(\"scenario %s: got %s()=%s, want: %s\", tt.scenario, nameOf(ctt.f), got, want)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "deploy/deployment.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package deploy contains information about all of the deployed instances, and how\n// they are organized across accounts, apps, regions, clusters, and autoscaling\n// groups.\npackage deploy\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/SmartThingsOSS/frigga-go\"\n)\n\n// Deployment contains information about how apps are deployed\ntype Deployment interface {\n\t// Apps sends App objects over a channel\n\tApps(c chan<- *App, appNames []string)\n\n\t// GetApp retrieves a single App\n\tGetApp(name string) (*App, error)\n\n\t// AppNames returns the names of all apps\n\tAppNames() ([]string, error)\n\n\t// GetInstanceIDs returns the ids for instances in a cluster\n\tGetInstanceIDs(app string, account AccountName, cloudProvider string, region RegionName, cluster ClusterName) (asgName ASGName, instances []InstanceID, err error)\n\n\t// GetClusterNames returns the list of cluster names\n\tGetClusterNames(app string, account AccountName) ([]ClusterName, error)\n\n\t// GetRegionNames returns the list of regions associated with a cluster\n\tGetRegionNames(app string, account AccountName, cluster ClusterName) ([]RegionName, error)\n\n\t// CloudProvider returns the provider associated with an account\n\tCloudProvider(account string) (provider string, err error)\n}\n\n// Account represents the set of clusters associated with an App that reside\n// in one AWS account (e.g., \"prod\", \"test\").\ntype Account struct {\n\tname          string // e.g., \"prod\", \"test\"\n\tclusters      []*Cluster\n\tapp           *App\n\tcloudProvider string // e.g., \"aws\"\n}\n\n// Name returns the name of the account associated with this account\nfunc (a *Account) Name() string {\n\treturn a.name\n}\n\n// Clusters returns a slice of clusters\nfunc (a *Account) Clusters() []*Cluster {\n\treturn a.clusters\n}\n\n// AppName returns the name of the app associated with this Account\nfunc (a *Account) AppName() string {\n\treturn a.app.name\n}\n\n// RegionNames returns the name of the regions that clusters in this account are\n// running in\nfunc (a *Account) RegionNames() []string {\n\tm := make(map[string]bool)\n\n\t// Get the region names of the clusters\n\tfor _, cluster := range a.Clusters() {\n\t\tfor _, name := range cluster.RegionNames() {\n\t\t\tm[name] = true\n\t\t}\n\t}\n\n\tresult := make([]string, 0, len(m))\n\tfor name := range m {\n\t\tresult = append(result, name)\n\t}\n\n\treturn result\n}\n\n// CloudProvider returns the cloud provider (e.g., \"aws\")\nfunc (a *Account) CloudProvider() string {\n\treturn a.cloudProvider\n}\n\ntype stringSet map[string]bool\n\nfunc (s *stringSet) add(val string) {\n\t(*s)[val] = true\n}\n\n// slice converts a stringSet to a string slice\nfunc (s stringSet) slice() []string {\n\tresult := []string{}\n\tfor val := range s {\n\t\tresult = append(result, val)\n\t}\n\treturn result\n}\n\n// StackNames returns the names of the stacks associated with this account\nfunc (a *Account) StackNames() []string {\n\tstacks := make(stringSet)\n\n\tfor _, cluster := range a.Clusters() {\n\t\tstacks.add(cluster.StackName())\n\t}\n\n\treturn stacks.slice()\n}\n\n// Cluster represents what Spinnaker refers to as a \"cluster\", which\n// contains app-stack-detail.\n// Every ASG is associated with exactly one cluster.\n// Note that clusters can span regions\ntype Cluster struct {\n\tname    string\n\tasgs    []*ASG\n\taccount *Account\n}\n\n// Name returns the name of the cluster, convention: app-stack-detail\nfunc (c *Cluster) Name() string {\n\treturn c.name\n}\n\n// AppName returns the name of the app associated with this cluster\nfunc (c *Cluster) AppName() string {\n\treturn c.account.AppName()\n}\n\n// StackName returns the name of the stack, following the app-stack-detail convention\nfunc (c *Cluster) StackName() string {\n\tnames, err := frigga.Parse(c.Name())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn names.Stack\n}\n\n// AccountName returns the name of the account associated with this cluster\nfunc (c *Cluster) AccountName() string {\n\treturn c.account.Name()\n}\n\n// ASGs returns a slice of ASGs\nfunc (c *Cluster) ASGs() []*ASG {\n\treturn c.asgs\n}\n\n// RegionNames returns the name of the region that this cluster runs in\nfunc (c *Cluster) RegionNames() []string {\n\tm := make(map[string]bool)\n\tfor _, asg := range c.ASGs() {\n\t\tm[asg.RegionName()] = true\n\t}\n\n\tresult := []string{}\n\tfor name := range m {\n\t\tresult = append(result, name)\n\t}\n\n\treturn result\n}\n\n// CloudProvider returns the cloud provider (e.g., \"aws\")\nfunc (c *Cluster) CloudProvider() string {\n\treturn c.account.CloudProvider()\n}\n\n// Instance implements instance.Instance\ntype Instance struct {\n\t// instance id (e.g., \"i-74e93ddb\")\n\tid string\n\n\t// ASG that this instance is part of\n\tasg *ASG\n}\n\nfunc (i *Instance) String() string {\n\treturn fmt.Sprintf(\"app=%s account=%s region=%s stack=%s cluster=%s asg=%s instance-id=%s\",\n\t\ti.AppName(), i.AccountName(), i.RegionName(), i.StackName(), i.ClusterName(), i.ASGName(), i.ID())\n}\n\n// AppName returns the name of the app associated with this instance\nfunc (i *Instance) AppName() string {\n\treturn i.asg.AppName()\n}\n\n// AccountName returns the name of the AWS account associated with the instance\nfunc (i *Instance) AccountName() string {\n\treturn i.asg.AccountName()\n}\n\n// ClusterName returns the name of the cluster associated with the instance\nfunc (i *Instance) ClusterName() string {\n\treturn i.asg.ClusterName()\n}\n\n// RegionName returns the name of the region associated with the instance\nfunc (i *Instance) RegionName() string {\n\treturn i.asg.RegionName()\n}\n\n// ASGName returns the name of the ASG associated with the instance\nfunc (i *Instance) ASGName() string {\n\treturn i.asg.Name()\n}\n\n// StackName returns the name of the stack associated with the instance\nfunc (i *Instance) StackName() string {\n\treturn i.asg.StackName()\n}\n\n// CloudProvider returns the cloud provider (e.g., \"aws\")\nfunc (i *Instance) CloudProvider() string {\n\treturn i.asg.CloudProvider()\n}\n\n// ID returns the instance id\nfunc (i *Instance) ID() string {\n\treturn i.id\n}\n"
  },
  {
    "path": "deploy/eligible_instance_groups.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage deploy\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n)\n\n// EligibleInstanceGroups returns a slice of InstanceGroups that represent\n// groups of instances that are eligible for termination.\n//\n// Note that this code does not check for violations of minimum time between\n// terminations. Chaos Monkey checks that precondition immediately before\n// termination, not when considering groups of eligible instances.\n//\n// The way instances are divided into group will depend on\n//   - the grouping configuration for the app (cluster, stack, app)\n//   - whether regions are independent\n//\n// The returned InstanceGroups are guaranteed to contain at least one instance\n// each\n//\n// Preconditions:\n//   - app is enabled for Chaos Monkey\nfunc (app *App) EligibleInstanceGroups(cfg chaosmonkey.AppConfig) []grp.InstanceGroup {\n\tif !cfg.Enabled {\n\t\tlog.Fatalf(\"app %s unexpectedly disabled\", app.Name())\n\t}\n\n\tgrouping := cfg.Grouping\n\tindep := cfg.RegionsAreIndependent\n\n\tswitch {\n\tcase grouping == chaosmonkey.App && indep:\n\t\treturn appIndep(app)\n\tcase grouping == chaosmonkey.App && !indep:\n\t\treturn appDep(app)\n\tcase grouping == chaosmonkey.Stack && indep:\n\t\treturn stackIndep(app)\n\tcase grouping == chaosmonkey.Stack && !indep:\n\t\treturn stackDep(app)\n\tcase grouping == chaosmonkey.Cluster && indep:\n\t\treturn clusterIndep(app)\n\tcase grouping == chaosmonkey.Cluster && !indep:\n\t\treturn clusterDep(app)\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"Unknown grouping: %d\", grouping))\n\t}\n}\n\n// appindep returns a list of groups grouped by (app, account, region)\nfunc appIndep(app *App) []grp.InstanceGroup {\n\tresult := []grp.InstanceGroup{}\n\tfor _, account := range app.accounts {\n\t\tfor _, regionName := range account.RegionNames() {\n\t\t\tresult = append(result, grp.New(app.Name(), account.Name(), regionName, \"\", \"\"))\n\t\t}\n\t}\n\treturn result\n}\n\n// stackIndep returns a list of groups grouped by (app, account)\nfunc appDep(app *App) []grp.InstanceGroup {\n\tresult := []grp.InstanceGroup{}\n\tfor _, account := range app.accounts {\n\t\tresult = append(result, grp.New(app.Name(), account.Name(), \"\", \"\", \"\"))\n\t}\n\treturn result\n}\n\n// stackIndep returns a list of groups grouped by (app, account, stack, region)\nfunc stackIndep(app *App) []grp.InstanceGroup {\n\n\ttype asr struct {\n\t\taccount string\n\t\tstack   string\n\t\tregion  string\n\t}\n\n\tset := make(map[asr]bool)\n\n\tfor _, account := range app.Accounts() {\n\t\tfor _, cluster := range account.Clusters() {\n\t\t\tstackName := cluster.StackName()\n\t\t\tfor _, regionName := range cluster.RegionNames() {\n\t\t\t\tset[asr{account: account.Name(), stack: stackName, region: regionName}] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tresult := []grp.InstanceGroup{}\n\tfor x := range set {\n\t\tresult = append(result, grp.New(app.Name(), x.account, x.region, x.stack, \"\"))\n\t}\n\n\treturn result\n}\n\n// stackDep returns a list of groups grouped by (app, account, stack)\nfunc stackDep(app *App) []grp.InstanceGroup {\n\tresult := []grp.InstanceGroup{}\n\tfor _, account := range app.accounts {\n\t\tfor _, stackName := range account.StackNames() {\n\t\t\tresult = append(result, grp.New(app.Name(), account.Name(), \"\", stackName, \"\"))\n\t\t}\n\t}\n\n\treturn result\n}\n\n// clusterDep returns a list of groups grouped by (app, account, cluster, region)\nfunc clusterIndep(app *App) []grp.InstanceGroup {\n\tresult := []grp.InstanceGroup{}\n\tfor _, account := range app.accounts {\n\t\tfor _, cluster := range account.Clusters() {\n\t\t\tfor _, regionName := range cluster.RegionNames() {\n\t\t\t\tresult = append(result, grp.New(app.Name(), account.Name(), regionName, \"\", cluster.Name()))\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\n// clusterDep returns a list of groups grouped by (app, account, cluster)\nfunc clusterDep(app *App) []grp.InstanceGroup {\n\tresult := []grp.InstanceGroup{}\n\tfor _, account := range app.accounts {\n\t\tfor _, cluster := range account.Clusters() {\n\t\t\tresult = append(result, grp.New(app.Name(), account.Name(), \"\", \"\", cluster.Name()))\n\t\t}\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "deploy/eligible_instance_groups_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage deploy\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n)\n\ntype groupList []grp.InstanceGroup\n\nvar grouptests = []struct {\n\tcfg    chaosmonkey.AppConfig\n\tgroups []grp.InstanceGroup\n}{\n\t{conf(chaosmonkey.App, false), groupList{\n\t\tgrp.New(\"mock\", \"prod\", \"\", \"\", \"\"),\n\t\tgrp.New(\"mock\", \"test\", \"\", \"\", \"\"),\n\t}},\n\t{conf(chaosmonkey.App, true), groupList{\n\t\tgrp.New(\"mock\", \"prod\", \"us-east-1\", \"\", \"\"),\n\t\tgrp.New(\"mock\", \"prod\", \"us-west-2\", \"\", \"\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-east-1\", \"\", \"\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-west-2\", \"\", \"\"),\n\t}},\n\t{conf(chaosmonkey.Stack, false), groupList{\n\t\tgrp.New(\"mock\", \"prod\", \"\", \"prod\", \"\"),\n\t\tgrp.New(\"mock\", \"prod\", \"\", \"staging\", \"\"),\n\t\tgrp.New(\"mock\", \"test\", \"\", \"test\", \"\"),\n\t\tgrp.New(\"mock\", \"test\", \"\", \"beta\", \"\"),\n\t}},\n\t{conf(chaosmonkey.Stack, true), groupList{\n\t\tgrp.New(\"mock\", \"prod\", \"us-east-1\", \"prod\", \"\"),\n\t\tgrp.New(\"mock\", \"prod\", \"us-west-2\", \"prod\", \"\"),\n\t\tgrp.New(\"mock\", \"prod\", \"us-east-1\", \"staging\", \"\"),\n\t\tgrp.New(\"mock\", \"prod\", \"us-west-2\", \"staging\", \"\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-east-1\", \"test\", \"\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-west-2\", \"test\", \"\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-east-1\", \"beta\", \"\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-west-2\", \"beta\", \"\"),\n\t}},\n\t{conf(chaosmonkey.Cluster, false), groupList{\n\t\tgrp.New(\"mock\", \"prod\", \"\", \"\", \"mock-prod-a\"),\n\t\tgrp.New(\"mock\", \"prod\", \"\", \"\", \"mock-prod-b\"),\n\t\tgrp.New(\"mock\", \"prod\", \"\", \"\", \"mock-staging-a\"),\n\t\tgrp.New(\"mock\", \"prod\", \"\", \"\", \"mock-staging-b\"),\n\t\tgrp.New(\"mock\", \"test\", \"\", \"\", \"mock-test-a\"),\n\t\tgrp.New(\"mock\", \"test\", \"\", \"\", \"mock-test-b\"),\n\t\tgrp.New(\"mock\", \"test\", \"\", \"\", \"mock-beta-a\"),\n\t\tgrp.New(\"mock\", \"test\", \"\", \"\", \"mock-beta-b\"),\n\t}},\n\t{conf(chaosmonkey.Cluster, true), groupList{\n\t\tgrp.New(\"mock\", \"prod\", \"us-east-1\", \"\", \"mock-prod-a\"),\n\t\tgrp.New(\"mock\", \"prod\", \"us-west-2\", \"\", \"mock-prod-a\"),\n\t\tgrp.New(\"mock\", \"prod\", \"us-east-1\", \"\", \"mock-prod-b\"),\n\t\tgrp.New(\"mock\", \"prod\", \"us-west-2\", \"\", \"mock-prod-b\"),\n\t\tgrp.New(\"mock\", \"prod\", \"us-east-1\", \"\", \"mock-staging-a\"),\n\t\tgrp.New(\"mock\", \"prod\", \"us-west-2\", \"\", \"mock-staging-a\"),\n\t\tgrp.New(\"mock\", \"prod\", \"us-east-1\", \"\", \"mock-staging-b\"),\n\t\tgrp.New(\"mock\", \"prod\", \"us-west-2\", \"\", \"mock-staging-b\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-east-1\", \"\", \"mock-test-a\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-west-2\", \"\", \"mock-test-a\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-east-1\", \"\", \"mock-test-b\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-west-2\", \"\", \"mock-test-b\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-east-1\", \"\", \"mock-beta-a\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-west-2\", \"\", \"mock-beta-a\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-east-1\", \"\", \"mock-beta-b\"),\n\t\tgrp.New(\"mock\", \"test\", \"us-west-2\", \"\", \"mock-beta-b\"),\n\t}},\n}\n\nfunc TestEligibleInstanceGroups(t *testing.T) {\n\tfor i, tt := range grouptests {\n\t\tgroups := mockApp.EligibleInstanceGroups(tt.cfg)\n\t\tif len(tt.groups) != len(groups) {\n\t\t\tt.Errorf(\"test %d: incorrect number of groups. Expected: %d. Actual: %d\", i, len(tt.groups), len(groups))\n\t\t\tcontinue\n\t\t}\n\n\t\tif !same(tt.groups, groups) {\n\t\t\tt.Errorf(\"test %d. Expected: %+v. Actual: %+v\", i, tt.groups, groups)\n\t\t}\n\t}\n}\n\n//\n// Test helper code\n//\n\n// conf creates a config file used for testing\nfunc conf(grouping chaosmonkey.Group, regionsAreIndependent bool) chaosmonkey.AppConfig {\n\treturn chaosmonkey.AppConfig{\n\t\tEnabled:                        true,\n\t\tRegionsAreIndependent:          regionsAreIndependent,\n\t\tMeanTimeBetweenKillsInWorkDays: 5,\n\t\tMinTimeBetweenKillsInWorkDays:  1,\n\t\tGrouping:                       grouping,\n\t}\n}\n\ntype groupSet map[grp.InstanceGroup]bool\n\nfunc (gs *groupSet) add(group grp.InstanceGroup) {\n\t(*gs)[group] = true\n}\n\nfunc (gl groupList) toSet() groupSet {\n\tresult := make(groupSet)\n\tfor _, group := range gl {\n\t\tresult.add(group)\n\t}\n\treturn result\n}\n\n// same return true if the two lists of groups contain the same elements,\n// independent of order\nfunc same(x, y groupList) bool {\n\tsx := x.toSet()\n\tsy := y.toSet()\n\treturn reflect.DeepEqual(sx, sy)\n}\n\nvar usEast1 = RegionName(\"us-east-1\")\nvar usWest2 = RegionName(\"us-west-2\")\n\nvar mockApp = NewApp(\"mock\", AppMap{\n\n\tAccountName(\"prod\"): {\n\t\tCloudProvider: \"aws\",\n\t\tClusters: ClusterMap{\n\t\t\tClusterName(\"mock-prod-a\"): {\n\t\t\t\tusEast1: {\n\t\t\t\t\tASGName(\"mock-prod-a-v123\"): []InstanceID{\"i-4a003cd0\"},\n\t\t\t\t},\n\t\t\t\tusWest2: {\n\t\t\t\t\tASGName(\"mock-prod-a-v111\"): []InstanceID{\"i-efdc42dc\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClusterName(\"mock-prod-b\"): {\n\t\t\t\tusEast1: {\n\t\t\t\t\tASGName(\"mock-prod-b-v002\"): []InstanceID{\"i-115ccc27\"},\n\t\t\t\t},\n\t\t\t\tusWest2: {\n\t\t\t\t\tASGName(\"mock-prod-b-v001\"): []InstanceID{\"i-7881287e\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClusterName(\"mock-staging-a\"): {\n\t\t\t\tusEast1: {\n\t\t\t\t\tASGName(\"mock-staging-a-v123\"): []InstanceID{\"i-ff8e7e4b\"},\n\t\t\t\t},\n\t\t\t\tusWest2: {\n\t\t\t\t\tASGName(\"mock-staging-a-v111\"): []InstanceID{\"i-6eed18a4\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClusterName(\"mock-staging-b\"): {\n\t\t\t\tusEast1: {\n\t\t\t\t\tASGName(\"mock-staging-b-v002\"): []InstanceID{\"i-13770e40\"},\n\t\t\t\t},\n\t\t\t\tusWest2: {\n\t\t\t\t\tASGName(\"mock-staging-b-v001\"): []InstanceID{\"i-afb7595e\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\tAccountName(\"test\"): {\n\t\tCloudProvider: \"aws\",\n\t\tClusters: ClusterMap{\n\t\t\tClusterName(\"mock-test-a\"): {\n\t\t\t\tusEast1: {\n\t\t\t\t\tASGName(\"mock-test-a-v123\"): []InstanceID{\"i-23b61f89\"},\n\t\t\t\t},\n\t\t\t\tusWest2: {\n\t\t\t\t\tASGName(\"mock-test-a-v111\"): []InstanceID{\"i-fe7a0827\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClusterName(\"mock-test-b\"): {\n\t\t\t\tusEast1: {\n\t\t\t\t\tASGName(\"mock-test-b-v002\"): []InstanceID{\"i-f581d5c3\"},\n\t\t\t\t},\n\t\t\t\tusWest2: {\n\t\t\t\t\tASGName(\"mock-test-b-v001\"): []InstanceID{\"i-986e988a\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClusterName(\"mock-beta-a\"): {\n\t\t\t\tusEast1: {\n\t\t\t\t\tASGName(\"mock-beta-a-v123\"): []InstanceID{\"i-4b359d5d\"},\n\t\t\t\t},\n\t\t\t\tusWest2: {\n\t\t\t\t\tASGName(\"mock-beta-a-v111\"): []InstanceID{\"i-e751bdd2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClusterName(\"mock-beta-b\"): {\n\t\t\t\tusEast1: {\n\t\t\t\t\tASGName(\"mock-beta-b-v002\"): []InstanceID{\"i-e5eeba5e\"},\n\t\t\t\t},\n\t\t\t\tusWest2: {\n\t\t\t\t\tASGName(\"mock-beta-b-v001\"): []InstanceID{\"i-76013ffb\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n})\n"
  },
  {
    "path": "deps/deps.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package deps holds a set of interfaces\npackage deps\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/clock\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedule\"\n)\n\nvar (\n\t// GetTrackers returns a list of trackers\n\t// This variable must be set in the init() method of a module imported by\n\t// the main module.\n\tGetTrackers func(*config.Monkey) ([]chaosmonkey.Tracker, error)\n\n\t// GetErrorCounter returns an error counter\n\tGetErrorCounter func(*config.Monkey) (chaosmonkey.ErrorCounter, error)\n\n\t// GetDecryptor returns a decryptor\n\tGetDecryptor func(*config.Monkey) (chaosmonkey.Decryptor, error)\n\n\t// GetEnv returns info about the deployed environment\n\tGetEnv func(*config.Monkey) (chaosmonkey.Env, error)\n\n\t// GetOutage returns an interface for checking if there is an outage\n\tGetOutage func(*config.Monkey) (chaosmonkey.Outage, error)\n\n\t// GetConstrainer returns an interface for constraining the schedule\n\tGetConstrainer func(*config.Monkey) (schedule.Constrainer, error)\n)\n\n// Deps are a common set of external dependencies\ntype Deps struct {\n\tMonkeyCfg  *config.Monkey\n\tChecker    chaosmonkey.Checker\n\tConfGetter chaosmonkey.AppConfigGetter\n\tCl         clock.Clock\n\tDep        deploy.Deployment\n\tT          chaosmonkey.Terminator\n\tTrackers   []chaosmonkey.Tracker\n\tOu         chaosmonkey.Outage\n\tErrCounter chaosmonkey.ErrorCounter\n\tEnv        chaosmonkey.Env\n}\n"
  },
  {
    "path": "docs/Configuration-file-format.md",
    "content": "The config file is in [TOML] format.\n\nChaos Monkey will look for a file named `chaosmonkey.toml` in the following\nlocations:\n\n * `.` (current directory)\n * `/apps/chaosmonkey`\n * `/etc`\n * `/etc/chaosmonkey`\n\n## Example\n\nHere is an example configuration file:\n\n[TOML]: https://github.com/toml-lang/toml\n\n```\n[chaosmonkey]\nenabled = true\nschedule_enabled = true\nleashed = false\naccounts = [\"production\", \"test\"]\n\n[database]\nhost = \"dbhost.example.com\"\nname = \"chaosmonkey\"\nuser = \"chaosmonkey\"\nencrypted_password = \"securepasswordgoeshere\"\n\n[spinnaker]\nendpoint = \"http://spinnaker.example.com:8084\"\n```\n\nNote that while the field is called \"encrypted_password\", you should put the\nunencrypted version of your password here. Chaos Monkey currently only ships\nwith a no-op (do nothing) password decryptor.\n\n\n### Defaults\n\nThe following example shows all of the default values:\n\n```\n[chaosmonkey]\nenabled = false                    # if false, won't terminate instances when invoked\nleashed = true                     # if true, terminations are only simulated (logged only)\nschedule_enabled = false           # if true, will generate schedule of terminations each weekday\naccounts = []                      # list of Spinnaker accounts with chaos monkey enabled, e.g.: [\"prod\", \"test\"]\n\nstart_hour = 9                     # time during day when starts terminating\nend_hour = 15                      # time during day when stops terminating\n\n# tzdata format, see TZ column in https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\n# Other allowed values: \"UTC\", \"Local\"\ntime_zone = \"America/Los_Angeles\"  # time zone used by start.hour and end.hour\n\nterm_account = \"root\"              # account used to run the term_path command\n\nmax_apps = 2147483647              # max number of apps Chaos Monkey will schedule terminations for\n\n# location of command Chaos Monkey uses for doing terminations\nterm_path = \"/apps/chaosmonkey/chaosmonkey-terminate.sh\"\n\n# cron file that Chaos Monkey writes to each day for scheduling kills\ncron_path = \"/etc/cron.d/chaosmonkey-daily-terminations\"\n\n# decryption system for encrypted_password fields for spinnaker and database\ndecryptor = \"\"\n\n# event tracking systems that records chaos monkey terminations\ntrackers = []\n\n# metric collection systems that track errors for monitoring/alerting\nerror_counter = \"\"\n\n# outage checking system that tells chaos monkey if there is an ongoing outage\noutage_checker = \"\"\n\n[database]\nhost = \"\"                # database host\nport = 3306              # tcp port that the database is listening on\nuser = \"\"                # database user\nencrypted_password = \"\"  # password for database auth, encrypted by decryptor\nname = \"\"                # name of database that contains chaos monkey data\n\n[spinnaker]\nendpoint = \"\"           # spinnaker api url\ncertificate = \"\"        # path to p12 file when using client-side tls certs\nencrypted_password = \"\" # password used for p12 certificate, encrypted by decryptor\nuser = \"\"               # user associated with terminations, sent in API call to terminate\n\n# For dynamic configuration options, see viper docs\n[dynamic]\nprovider = \"\"   # options: \"etcd\", \"consul\"\nendpoint = \"\"   # url for dynamic provider\npath = \"\"       # path for dynamic provider\n```\n\nNote that many of these configuration parameters (decryptor, trackers,\nerror_counter, outage_checker) currently only have no-op implementations.\n"
  },
  {
    "path": "docs/Configuring-behavior-via-Spinnaker.md",
    "content": "Through the Spinnaker web UI, you can configure how often Chaos Monkey\nterminates instances for each application.\n\nClick on the \"Config\" tab in Spinnaker. There should be a \"Chaos Monkey\"\nwidget where you can enable/disable Chaos Monkey for the app, as well as\nconfigure its behavior.\n\n![Config screenshot](config.png)\n\n## Termination frequency\n\nBy default, Chaos Monkey is configured for a *mean time between terminations* of\ntwo (2) days, which means that on average Chaos Monkey will terminate an\ninstance every two days for each group in that app.\n\nThe lowest permitted value for mean time between terminations is one (1) day.\n\nChaos Monkey also has a *minimum time between terminations*, which defaults to\none (1) day. This means that Chaos Monkey is guaranteed to never kill more often\nthan once a day for each group. Even if multiple Chaos Monkeys are deployed, as\nlong as they are all configured to use the same database, they will obey the\nminimum time between terminations.\n\n### Grouping\n\nChaos Monkey operates on *groups* of instances. Every work day, for every\n(enabled) group of instances, Chaos Monkey will flip a biased coin to determine\nwhether it should kill an instance from a group. If so, it will randomly\nselect an instance from the group.\n\nUsers can configure what Chaos Monkey considers a group.  The three options are:\n\n* app\n* stack\n* cluster\n\nIf grouping is set to \"app\", Chaos Monkey will terminate up to one instance per\napp each day, regardless of how these instances are organized into clusters.\n\nIf the grouping is set to \"stack\", Chaos Monkey will terminate up to one instance per\nstack each day. For instance, if an application has three stacks defined, then\nChaos Monkey may kill up to three instances in this app per day.\n\nIf the grouping is set to \"cluster\", Chaos Monkey will terminate up to one\ninstance per cluster each day.\n\nBy default, Chaos Monkey treats each region separately. However, if the \"regions\nare independent\" option is unchecked, then Chaos Monkey will not terminate\ninstances that are in the same group but in different regions. This is intended\nto support databases that replicate across regions where simultaneous\ntermination across regions is undesirable.\n\n## Exceptions\n\nYou can opt-out combinations of account, region, stack, and detail. In the\nexample config shown above, Chaos Monkey will not terminate instances in the\nprod account in the us-west-2 region with a stack of \"staging\" and a blank\ndetail field.\n\nThe exception field also supports a wildcard, `*`, which matches everything. In\nthe example above, Chaos Monkey will also not terminate any instances in the\ntest account, regardless of region, stack or detail.\n"
  },
  {
    "path": "docs/How-to-deploy.md",
    "content": "We currently don't have a streamlined process for deploying Chaos Monkey. This\npage describes the manual steps required to build and deploy. A great way to\ncontribute to this project would be to use Docker containers to make it easier\nfor other users to get up and running quickly.\n\n## Prerequisites\n\n* [Spinnaker]\n* MySQL (8.0 or later)\n\nTo use this version of Chaos Monkey, you must be using [Spinnaker] to manage your applications. Spinnaker is the\ncontinuous delivery platform that we use at Netflix.\n\nChaos Monkey also requires a MySQL-compatible database, version 8.0 or later.\n\n[Spinnaker]: http://www.spinnaker.io/\n\n\n## Build\n\nTo build Chaos Monkey on your local machine (requires the Go\ntoolchain).\n\n```\ngo get github.com/netflix/chaosmonkey/cmd/chaosmonkey\n```\n\nThis will install a `chaosmonkey` binary in your `$GOBIN` directory.\n\n## How Chaos Monkey runs\n\nChaos Monkey does not run as a service. Instead, you set up a cron job\nthat calls Chaos Monkey once a weekday to create a schedule of terminations.\n\nWhen Chaos Monkey creates a schedule, it creates another cron job to schedule terminations\nduring the working hours of the day.\n\n## Deploy overview\n\nTo deploy Chaos Monkey, you need to:\n\n1. Configure Spinnaker for Chaos Monkey support\n1. Set up the MySQL database\n1. Write a configuration file (chaosmonkey.toml)\n1. Set up a cron job that runs Chaos Monkey daily schedule\n\n## Configure Spinnaker for Chaos Monkey support\n\nSpinnaker's web interface is called *Deck*. You need to be running Deck version\nv.2839.0 or greater for Chaos Monkey support. Check which version of Deck you are\nrunning by hitting the `/version.json` endpoint of your Spinnaker deployment.\n(Note that this version information will not be present if you are running\nDeck using a [Docker container hosted on Quay][quay]).\n\n[quay]: https://quay.io/repository/spinnaker/deck\n\nDeck has a config file named `/var/www/settings.js`. In this file there is a\n\"feature\" object that contains a number of feature flags:\n\n```\n  feature: {\n    pipelines: true,\n    notifications: false,\n    fastProperty: true,\n    ...\n```\n\nAdd the following flag:\n\n```\nchaosMonkey: true\n```\n\nIf the feature was enabled successfully, when you create a new app with Spinnaker, you will see\na \"Chaos Monkey: Enabled\" checkbox in the \"New Application\" modal dialog. If it\ndoes not appear, you may need to deploy a more recent version of Spinnaker.\n\n![new-app](new-app.png \"new application dialog\")\n\nFor more details, see [Additional configuration files][spinconfig] on the\nSpinnaker website.\n\n[spinconfig]: http://www.spinnaker.io/docs/custom-configuration#section-additional-configuration-files\n\n\n\n## Create the MySQL database\n\nChaos Monkey uses a MySQL database as a backend to record a daily termination\nschedule and to enforce a minimum time between terminations. (By default, Chaos\nMonkey will not terminate more than one instance per day per group).\n\nLog in to your MySQL deployment and create a database named `chaosmonkey`:\n\n```\nmysql> CREATE DATABASE chaosmonkey;\n```\n\nNote: Chaos Monkey does not currently include a mechanism for purging old data.\nUntil this function exists, it is the operator's responsibility to remove old\ndata as needed.\n\n## Write a configuration file (chaosmonkey.toml)\n\nSee [Configuration file format](Configuration-file-format) for the configuration file format.\n\n## Create the database schema\n\nOnce you have created a `chaosmonkey` database and have populated the\nconfiguration file with the database credentials, add the tables to the database\nby doing:\n\n```\nchaosmonkey migrate\n```\n\n\n### Verifying Chaos Monkey is configured properly\n\nChaos Monkey supports a number of command-line arguments that are useful for\nverifying that things are working properly.\n\n#### Spinnaker\n\nYou can verify that Chaos Monkey can reach Spinnaker by fetching the Chaos Monkey\nconfiguration for an app:\n\n```\nchaosmonkey config <appname>\n```\n\nIf successful, you'll see output that looks like:\n\n```\n(*chaosmonkey.AppConfig)(0xc4202ec0c0)({\n Enabled: (bool) true,\n RegionsAreIndependent: (bool) true,\n MeanTimeBetweenKillsInWorkDays: (int) 2,\n MinTimeBetweenKillsInWorkDays: (int) 1,\n Grouping: (chaosmonkey.Group) cluster,\n Exceptions: ([]chaosmonkey.Exception) {\n }\n})\n```\n\nIf it fails, you'll see an error message.\n\n#### Database\n\nYou can verify that Chaos Monkey can reach the database by attempting to\nretrieve the termination schedule for the day.\n\n```\nchaosmonkey fetch-schedule\n```\n\nIf successful, you should see output like:\n\n```\n[69400] 2016/09/30 23:41:03 chaosmonkey fetch-schedule starting\n[69400] 2016/09/30 23:41:03 Writing /etc/cron.d/chaosmonkey-daily-terminations\n[69400] 2016/09/30 23:41:03 chaosmonkey fetch-schedule done\n```\n\n(Chaos Monkey will write an empty file to\n`/etc/cron.d/chaosmonkey-daily-terminations` since the database does not contain\nany termination schedules yet).\n\nIf Chaos Monkey cannot reach the database, you will see an error. For example:\n\n```\n[69668] 2016/09/30 23:43:50 chaosmonkey fetch-schedule starting\n[69668] 2016/09/30 23:43:50 FATAL: could not fetch schedule: failed to retrieve schedule for 2016-09-30 23:43:50.953795019 -0700 PDT: dial tcp 127.0.0.1:3306: getsockopt: connection refused\n```\n\n#### Generate a termination schedule\n\nYou can manually invoke Chaos Monkey to generate a schedule file. When testing,\nyou may want to specify `--no-record-schedule` so the schedule doesn't get\nwritten to the database.\n\nIf you have many apps and you don't want to sit there while Chaos Monkey\ngenerates a complete schedule, you can limit the number of apps  using the\n`--max-apps=<number>`. For example:\n\n```\nchaosmonkey schedule --no-record-schedule --max-apps=10\n```\n\n#### Terminate an instance\n\nYou can manually invoke Chaos Monkey to terminate an instance. For example:\n\n```\nchaosmonkey terminate chaosguineapig test --cluster=chaosguineapig --region=us-east-1\n```\n\n\n### Optional: Dynamic properties (etcd, consul)\n\nChaos Monkey supports changing the following configuration properties dynamically:\n\n* chaosmonkey.enabled\n* chaosmonkey.leashed\n* chaosmonkey.schedule_enabled\n* chaosmonkey.accounts\n\nThese are intended to allow an operator to make certain changes to Chaos\nMonkey's behavior without having to redeploy.\n\nNote: the configuration file takes precedence over dynamic provider, so do\nnot specify these properties in the config file if you want to set them\ndynamically.\n\nTo take advantage of dynamic properties, you need to keep those properties in\neither [etcd] or [Consul] and add a `[dynamic]` section that contains the\nendpoint for the service and a path that returns a JSON file that has each of\nthe properties you want to set dynamically.\n\nChaos Monkey uses the [Viper][viper] library to implement dynamic configuration, see the\nViper [remote key/value store support][remote] docs for more details.\n\n\n[etcd]: https://coreos.com/etcd/docs/latest/\n[consul]: https://www.consul.io/\n[viper]: https://github.com/spf13/viper\n[remote]: https://github.com/spf13/viper#remote-keyvalue-store-support\n\n\n## Set up a cron job that runs Chaos Monkey daily schedule\n\n### Create /apps/chaosmonkey/chaosmonkey-schedule.sh\n\nFor the remainder if the docs, we assume you have copied the chaosmonkey binary\nto `/apps/chaosmonkey`, and will create the scripts described below there as\nwell. However, Chaos Monkey makes no explicit assumptions about the location of\nthese files.\n\n\nCreate a file called `chaosmonkey-schedule.sh` that invokes `chaosmonkey\nschedule` and writes the output to a logfile.\n\nNote that because this will be invoked from cron, the PATH will likely not include the\nlocation of the chaosmonkey binary so be sure to specify it explicitly.\n\n/apps/chaosmonkey/chaosmonkey-schedule.sh:\n```bash\n#!/bin/bash\n/apps/chaosmonkey/chaosmonkey schedule >> /var/log/chaosmonkey-schedule.log 2>&1\n```\n\n### Create /etc/cron.d/chaosmonkey-schedule\n\nOnce you have this script, create a cron job that invokes it once a day. Chaos\nMonkey starts terminating at `chaosmonkey.start_hour` in\n`chaosmonkey.time_zone`, so it's best to pick a time earlier in the day.\n\nThe example below generates termination schedules each weekday at 12:00 system\ntime (which we assume is in UTC).\n\n/etc/cron.d/chaosmonkey-schedule:\n```bash\n# Run the Chaos Monkey scheduler at 5AM PDT (4AM PST) every weekday\n# This corresponds to: 12:00 UTC\n# Because system clock runs UTC, time change affects when job runs\n\n# The scheduler must run as root because it needs root permissions to write\n# to the file /etc/cron.d/chaosmonkey-daily-terminations\n\n# min  hour  dom  month  day  user  command\n    0    12    *      *  1-5  root  /apps/chaosmonkey/chaosmonkey-schedule.sh\n```\n\n### Create /apps/chaosmonkey/chaosmonkey-terminate.sh\n\nWhen Chaos Monkey schedules terminations, it will create cron jobs that call the\npath specified by `chaosmonkey.term_path`, which defaults to /apps/chaosmonkey/chaosmonkey-terminate.sh\n\n/apps/chaosmonkey/chaosmonkey-terminate.sh:\n```\n#!/bin/bash\n/apps/chaosmonkey/chaosmonkey terminate \"$@\" >> /var/log/chaosmonkey-terminate.log 2>&1\n```\n\n"
  },
  {
    "path": "docs/Running-locally.md",
    "content": "*Note: this doc is in progress*\n\nTo run locally, you need a local MySQL and a local Spinnaker. This page\ndescribes how to start both of those up using Docker containers\n\n## MySQL\n\nThis will start up a MySQL container with the root password as `password`.\n\n```bash\ndocker run -e MYSQL_ROOT_PASSWORD=password -p3306:3306 mysql:8.0\n```\n"
  },
  {
    "path": "docs/Termination-behavior.md",
    "content": "## Enabled group\n\nChaos Monkey will only consider server groups eligible for termination if they\nare marked as enabled by Spinnaker.  The Spinnaker API exposes an *isDisabled*\nboolean flag to indicate whether a group is disabled. Chaos Monkey filters on\nthis to ensure that it only terminates from active groups.\n\n## Probability\n\nFor each app, Chaos Monkey divides the instances into instance groups (the groupings\ndepend on how the app is configured). Every weekday, for each instance group,\nChaos Monkey flips a weighted coin to decide whether to terminate an instance\nfrom that group. If the coin comes up heads, Chaos Monkey schedules a termination at\na random time between 9AM and 3PM that day.\n\nUnder this behavior, the number of work days between terminations for an\ninstance group is a random variable that has a [geometric distribution][1].\n\nThe equation below describes the probability distribution for the time between\nterminations. *X* is the random variable, *n* is the number of work days between\nterminations, and *p* is the probability that the coin comes up heads.\n\n    P(X=n) = (1-p)^(n-1) × p,   n>=1\n\nTaking expectation over *X* gives the mean:\n\n    E[X] = 1/p\n\nEach app defines two parameters that governs how often Chaos Monkey should terminate\ninstances for that app:\n\n * mean time between terminations in work days (μ)\n * min time between terminations in work days  (ɛ)\n\nChaos Monkey uses μ to determine what *p* should be. If we ignore the effect of\nɛ and solve for *p*:\n\n    μ = E[X] = 1/p\n    p = 1/μ\n\nAs an example, for a given app, assume that μ=5. On each day, the probability of\na termination is 1/5.\n\nNote that if ɛ>1, Chaos Monkey termination behavior is no longer\na geometric distribution:\n\n    P(X=n) = (1-p)^(n-1) × p,  n>=ɛ\n\n\nIn particular, as ɛ grows larger, E[X]-μ gets larger. We don't apply a\ncorrection for this, because the additional complexity in the math isn't worth\nhaving E[X] exactly equal μ.\n\nAlso note that if μ=1, then p=1, which guarantees a termination each day.\n\n\n\n[1]: https://en.wikipedia.org/wiki/Geometric_distribution\n"
  },
  {
    "path": "docs/dev/Running-tests.md",
    "content": "To run unit tests:\n\n```bash\ngo test ./...\n```\n\n## Tests that interact with MySQL\n\nThere are some tests that interact with MySQL. the test files are\n`mysql/*_test.go`\n\nThese tests assume a MySQL deployment at the following connection string:\n\n```\nroot:password@tcp(127.0.0.1:3306)/\n```\n\n### Testing with Docker\n\nThe simplest way to run these tests is to install Docker on your local machine.\nThese tests use the `mysql:8.0` container (version 8.0 is used to ensure\ncompatibility with [Amazon Aurora][1]).\n\nNote that if you are on macOS, you must use [Docker for Mac][2], not Docker\nToolbox. Otherwise, the Docker containers will not be accessible at 127.0.0.1.\n\n\nIf you want to run these tests, ensure you have Docker installed locally, and\ngrab the mysql:8.0 container:\n\n```bash\ndocker pull mysql:8.0\n```\n\nThen run the tests with the `docker` tag, like this:\n\n```\ngo test -tags docker  ./...\n```\n\nThe tests will automatically start the mysql container and then bring it down.\n\n### Testing without bringing Docker container up and down\n\nIf you don't want the tests to bring the mysql Docker container up and down each\ntime (e.g., you want to run the tests more quickly, or you want to test by\nrunning a mysql instance natively), use the \"dockerup\" flag along with the\n\"docker\" flag.\n\n```\ngo test -tags \"docker dockerup\"  ./...\n```\n\n(In retrospect, \"docker\" and \"dockerup\" are not great names for these tag, maybe \"mysqltests\"\nand \"nodocker\" would be better).\n\n[1]: https://aws.amazon.com/rds/aurora\n[2]: https://docs.docker.com/engine/installation/mac/\n"
  },
  {
    "path": "docs/dev/Vendoring-dependencies.md",
    "content": "If you wish to add a new dependency to Chaos Monkey, use [govendor][1] to add it.\n\nPlease ensure that the license of the new dependency is compatible with Chaos Monkey's license: [Apache License Version 2.0][2].\n\n\n[1]: https://github.com/kardianos/govendor\n[2]: https://github.com/Netflix/chaosmonkey/blob/master/LICENSE"
  },
  {
    "path": "docs/index.md",
    "content": "![Logo](logo.png)\n\nChaos Monkey is responsible for randomly terminating instances in production to ensure that engineers implement their services to be resilient to instance failures.\n\nSee [how to deploy](How-to-deploy) for instructions on how to get up and running with Chaos Monkey.\n\nOnce you're up and running, see [configuring behavior via Spinnaker](Configuring-behavior-via-Spinnaker) for how users can customize the behavior of Chaos Monkey for their apps.\n"
  },
  {
    "path": "docs/plugins/Constrainer.md",
    "content": "# Constrainer\n\nThere may be some cases where you want to prevent some combination of Chaos\nMonkey terminations, but the [configuration options](../Configuring-behavior-via-spinnaker) aren't flexible\nenough for your use case.\n\nYou can define a custom constrainer to do this.\n\nAs an example, let's say you wanted to disallow any terminations for apps\nthat contain \"foo\" as a substring.\n\n```go\npackage constrainer\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/deps\"\n\t\"github.com/Netflix/chaosmonkey/config\"\n\t\"github.com/Netflix/chaosmonkey/schedule\"\n    \"strings\"\n)\n\nfunc init() {\n    deps.GetConstrainer = getConstrainer()\n}\n\ntype noFoo struct {}\n\nfunc getConstrainer(cfg *config.Monkey) (schedule.Constrainer, error) {\n    return noFoo{}, nil\n}\n\nfunc (n noFoo) Filter(s schedule.Schedule) schedule.Schedule {\n\tresult := schedule.New()\n\tfor _, entry := range s.Entries() {\n        if !strings.Contains(entry.Group.App(), \"foo\") {\n            result.Add(entry.Time, entry.Group)\n        }\n    }\n    return result\n}\n\n```\n\nSee the [Plugins](index.md) page for info on how to build a custom version of\nChaos Monkey with your plugin.\n"
  },
  {
    "path": "docs/plugins/Decryptor.md",
    "content": "A decryptor allows you to use encrypted versions of the passwords for the MySQL\ndatabase and Spinnaker p12 certificate (see [configuration file format](Configuration-file-format)).\nChaos Monkey will invoke the decryptor to decrypt the passwords before using\nthem.\n\nChaos Monkey does not ship with any decryptor implementations. If you wish to\nuse this functionality, you will need to implement your own.\n\n\nIf you wish to store your passwords encrypted and use a decryption system at\nruntime, you need to:\n\n1. Give your decryptor a name (e.g., \"gpg\")\n1. Code up a type in Go that implements the [Decryptor](https://godoc.org/github.com/Netflix/chaosmonkey/#Decryptor) interface.\n1. Modify [decryptor.go](https://github.com/Netflix/chaosmonkey/blob/master/decryptor/decryptor.go) so that it recognizes your decryptor.\n1. Edit your [config file](Configuration-file-format) to specify your decryptor.\n\n\n"
  },
  {
    "path": "docs/plugins/Error-counter.md",
    "content": "An error counter is used to record the rate of errors generated by Chaos Monkey\nto an external system such as a metrics or alerting system.\n\nInside of Netflix, we use an error counter to record error counts to [Atlas](https://github.com/netflix/atlas/wiki), our metric system<sup>1</sup>.\n\nIf you wish to record the error counts with an external system, you need to:\n\n1. Give your error counter a name (e.g., \"ganglia\")\n1. Code up a type in Go that implements the [ErrorCounter](https://godoc.org/github.com/Netflix/chaosmonkey/#ErrorCounter) interface\n1. Modify [errorcounter.go](https://github.com/Netflix/chaosmonkey/blob/master/errorcounter/errorcounter.go) so that it recognizes your error counter.\n1. Edit your [config file](Configuration File Format) to specify your error counter.\n\n---\n\n<sup>1</sup>Unfortunately, we are unable to release this error counter as\nopen source. Our Atlas error counter communicates with a version of\n[Prana](https://github.com/Netflix/Prana) that has not been released as open\nsource.\n"
  },
  {
    "path": "docs/plugins/Outage-checker.md",
    "content": "An outage checker is used to automatially disable Chaos Monkey during ongoing outages.\n\nIf you wish to have Chaos Monkey check if there is an ongoing outage and disable\naccordingly, you need to:\n\n1. Give your outage checker a name (e.g., \"chatbot\")\n1. Code up a type in Go that implements the [Outage](https://godoc.org/github.com/netflix/chaosmonkey/#Outage) interface.\n1. Modify [outage.go](https://github.com/Netflix/chaosmonkey/blob/master/outage/outage.go) so that it recognizes your outage checker.\n1. Edit your [config file](Configuration File Format) to specify your outage checker.\n"
  },
  {
    "path": "docs/plugins/Tracker.md",
    "content": "A tracker is used to record termination events in some sort of external system.\nInside Netflix, we use trackers to record terminations to\n[Atlas](https://github.com/netflix/atlas/wiki) (our metrics system) and to\nChronos, our event tracking system<sup>1</sup>.\n\nIf you wish to record terminations with some external system, you need to:\n\n1. Give your tracker a name (e.g., \"syslog\")\n1. Code up a type in Go that implements the [Tracker](https://godoc.org/github.com/Netflix/chaosmonkey/#Tracker) interface.\n1. Modify [github.com/netflix/chaosmonkey/tracker/getTracker](https://github.com/Netflix/chaosmonkey/blob/master/tracker/tracker.go)\n   so that it recognizes your tracker.\n1. Edit your [config file](Configuration File Format) to specify your tracker.\n\n---\n\n<sup>1</sup>Unfortunately, we are unable to release either of these trackers as\nopen source. Our Atlas tracker communicates with a version of\n[Prana](https://github.com/Netflix/Prana) that has not been released as open\nsource, and Chronos has also not been released as open source.\n"
  },
  {
    "path": "docs/plugins/index.md",
    "content": "# Plugins\n\nWhen Chaos Monkey runs inside of Netflix, it integrates with a number of\nproprietary systems and contains some Netflix-specific business logic. For example:\n\n* Terminations are logged with an internal event tracking system \n* Metrics are logged to an internal metrics system.\n* Credentials are decrypted using an internal secrets system.\n* Dynamic configuration properties are retrieved from an internal\n  configuration system.\n* Some custom rules that prevent certain termination combinations from\n  occurring.\n\nIn order to support  release Chaos Monkey as open source, these proprietary\nintegrations are implemented as *plugins* that aren't released. Chaos Monkey\nships with no-op implementations of these plugins.\n\n\n## Building Chaos Monkey with custom plugins\n\nAs an example, let's say you wished to implement a custom\n[constrainer](Constrainer) for your organization.\n\nThis doc assumes that you will put the code in\n`$GOPATH/example.com/chaosmonkey`. You should substitute \"example.com\" with\nsomething relevant to your organization.\n\n### 1. Grab the open source Chaos Monkey source\n\nIf you haven't done this already, ensure the open source code is on your local\nmachine. You can use `go get` for this:\n\n    go get github.com/netflix/chaosmonkey/cmd/chaosmonkey\n\n### 2. Create a file with the custom constrainer implementation.\n\nFile: `$GOPATH/src/example.com/chaosmonkey/constrainer.go`\n\nSee the [Constrainer](Constrainer) page for an example implementation.\n\n\n### 3. Create the file that loads the plugins\n\nFile:  `$GOPATH/src/example.com/chasmonkey/cmd/chaosmonkey/main.go`\n\nIt looks like this:\n\n```go\npackage main\n\n\nimport (\n    \"github.com/Netflix/chaosmonkey/command\"\n\n    _ \"example.com/chaosmonkey/constrainer\"\n)\n\nfunc main() {\n    command.Execute()\n}\n```\n\n### 4. Build the custom Chaos Monkey binary\n\n```\ngo build example.com/chaosmonkey/cmd/chaosmonkey\n```\n"
  },
  {
    "path": "eligible/eligible.go",
    "content": "// Copyright 2017 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package eligible contains methods that determine which instances are eligible for Chaos Monkey termination\npackage eligible\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n\t\"github.com/SmartThingsOSS/frigga-go\"\n\t\"github.com/pkg/errors\"\n\t\"strings\"\n)\n\n// TODO: make these a configuration parameter\nvar neverEligibleSuffixes = []string{\"-canary\", \"-baseline\", \"-citrus\", \"-citrusproxy\"}\n\ntype (\n\tcluster struct {\n\t\tappName       deploy.AppName\n\t\taccountName   deploy.AccountName\n\t\tcloudProvider deploy.CloudProvider\n\t\tregionName    deploy.RegionName\n\t\tclusterName   deploy.ClusterName\n\t}\n\n\tinstance struct {\n\t\tappName       deploy.AppName\n\t\taccountName   deploy.AccountName\n\t\tregionName    deploy.RegionName\n\t\tstackName     deploy.StackName\n\t\tclusterName   deploy.ClusterName\n\t\tasgName       deploy.ASGName\n\t\tid            deploy.InstanceID\n\t\tcloudProvider deploy.CloudProvider\n\t}\n)\n\nfunc (i instance) AppName() string {\n\treturn string(i.appName)\n}\n\nfunc (i instance) AccountName() string {\n\treturn string(i.accountName)\n}\n\nfunc (i instance) RegionName() string {\n\treturn string(i.regionName)\n}\n\nfunc (i instance) StackName() string {\n\treturn string(i.stackName)\n}\n\nfunc (i instance) ClusterName() string {\n\treturn string(i.clusterName)\n}\n\nfunc (i instance) ASGName() string {\n\treturn string(i.asgName)\n}\n\nfunc (i instance) Name() string {\n\treturn string(i.clusterName)\n}\n\nfunc (i instance) ID() string {\n\treturn string(i.id)\n}\n\nfunc (i instance) CloudProvider() string {\n\treturn string(i.cloudProvider)\n}\n\nfunc isException(exs []chaosmonkey.Exception, account deploy.AccountName, names *frigga.Names, region deploy.RegionName) bool {\n\tfor _, ex := range exs {\n\t\tif ex.Matches(string(account), names.Stack, names.Detail, string(region)) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc isNeverEligible(cluster deploy.ClusterName) bool {\n\tfor _, suffix := range neverEligibleSuffixes {\n\t\tif strings.HasSuffix(string(cluster), suffix) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc clusters(group grp.InstanceGroup, cloudProvider deploy.CloudProvider, exs []chaosmonkey.Exception, dep deploy.Deployment) ([]cluster, error) {\n\taccount := deploy.AccountName(group.Account())\n\tclusterNames, err := dep.GetClusterNames(group.App(), account)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := make([]cluster, 0)\n\tfor _, clusterName := range clusterNames {\n\t\tnames, err := frigga.Parse(string(clusterName))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdeployedRegions, err := dep.GetRegionNames(names.App, account, clusterName)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor _, region := range regions(group, deployedRegions) {\n\n\t\t\tif isException(exs, account, names, region) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif isNeverEligible(clusterName) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif grp.Contains(group, string(account), string(region), string(clusterName)) {\n\t\t\t\tresult = append(result, cluster{\n\t\t\t\t\tappName:       deploy.AppName(names.App),\n\t\t\t\t\taccountName:   account,\n\t\t\t\t\tcloudProvider: cloudProvider,\n\t\t\t\t\tregionName:    region,\n\t\t\t\t\tclusterName:   clusterName,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// regions returns list of candidate regions for termination given app config and where cluster is deployed\nfunc regions(group grp.InstanceGroup, deployedRegions []deploy.RegionName) []deploy.RegionName {\n\tregion, ok := group.Region()\n\tif ok {\n\t\treturn regionsWhenTermScopedtoSingleRegion(region, deployedRegions)\n\t}\n\n\treturn deployedRegions\n}\n\n// regionsWhenTermScopedtoSingleRegion returns a list containing either the region or empty, depending on whether the region is one of the deployed ones\nfunc regionsWhenTermScopedtoSingleRegion(region string, deployedRegions []deploy.RegionName) []deploy.RegionName {\n\tif contains(region, deployedRegions) {\n\t\treturn []deploy.RegionName{deploy.RegionName(region)}\n\t}\n\n\treturn nil\n}\n\nfunc contains(region string, regions []deploy.RegionName) bool {\n\tfor _, r := range regions {\n\t\tif region == string(r) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nconst whiteListErrorMessage = \"whitelist is not supported\"\n\n// isWhiteList returns true if an error is related to a whitelist\nfunc isWhitelist(err error) bool {\n\treturn err.Error() == whiteListErrorMessage\n}\n\n// Instances returns instances eligible for termination\nfunc Instances(group grp.InstanceGroup, exs []chaosmonkey.Exception, dep deploy.Deployment) ([]chaosmonkey.Instance, error) {\n\tcloudProvider, err := dep.CloudProvider(group.Account())\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"retrieve cloud provider failed\")\n\t}\n\n\tcls, err := clusters(group, deploy.CloudProvider(cloudProvider), exs, dep)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := make([]chaosmonkey.Instance, 0)\n\n\tfor _, cl := range cls {\n\t\tinstances, err := getInstances(cl, dep)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresult = append(result, instances...)\n\n\t}\n\treturn result, nil\n\n}\n\nfunc getInstances(cl cluster, dep deploy.Deployment) ([]chaosmonkey.Instance, error) {\n\tresult := make([]chaosmonkey.Instance, 0)\n\n\tasgName, ids, err := dep.GetInstanceIDs(string(cl.appName), cl.accountName, string(cl.cloudProvider), cl.regionName, cl.clusterName)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, id := range ids {\n\t\tnames, err := frigga.Parse(string(asgName))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to parse\")\n\t\t}\n\t\tresult = append(result,\n\t\t\tinstance{appName: cl.appName,\n\t\t\t\taccountName:   cl.accountName,\n\t\t\t\tregionName:    cl.regionName,\n\t\t\t\tstackName:     deploy.StackName(names.Stack),\n\t\t\t\tclusterName:   cl.clusterName,\n\t\t\t\tasgName:       deploy.ASGName(asgName),\n\t\t\t\tid:            id,\n\t\t\t\tcloudProvider: cl.cloudProvider,\n\t\t\t})\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "eligible/eligible_test.go",
    "content": "package eligible\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\tD \"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n\t\"github.com/Netflix/chaosmonkey/v2/mock\"\n\t\"sort\"\n\t\"testing\"\n)\n\nfunc mockDeployment() D.Deployment {\n\ta := D.AccountName(\"prod\")\n\tp := \"aws\"\n\tr1 := D.RegionName(\"us-east-1\")\n\tr2 := D.RegionName(\"us-west-2\")\n\n\treturn &mock.Deployment{AppMap: map[string]D.AppMap{\n\t\t\"foo\": {a: D.AccountInfo{CloudProvider: p, Clusters: D.ClusterMap{\n\t\t\t\"foo-crit\": {\n\t\t\t\tr1: {\"foo-crit-v001\": []D.InstanceID{\"i-11111111\", \"i-22222222\"}},\n\t\t\t\tr2: {\"foo-crit-v001\": []D.InstanceID{\"i-aaaaaaaa\", \"i-bbbbbbbb\"}}},\n\t\t\t\"foo-crit-lorin\": {\n\t\t\t\tr1: {\"foo-crit-lorin-v123\": []D.InstanceID{\"i-33333333\", \"i-44444444\"}}},\n\t\t\t\"foo-staging\": {\n\t\t\t\tr1: {\"foo-staging-v005\": []D.InstanceID{\"i-55555555\", \"i-66666666\"}},\n\t\t\t\tr2: {\"foo-staging-v005\": []D.InstanceID{\"i-cccccccc\", \"i-dddddddd\"}},\n\t\t\t},\n\t\t\t\"foo-staging-lorin\": {r1: {\"foo-crit-lorin-v117\": []D.InstanceID{\"i-77777777\", \"i-88888888\"}}},\n\t\t}},\n\t\t}}}\n}\n\n// ids returns a sorted list of instance ids\nfunc ids(instances []chaosmonkey.Instance) []string {\n\tresult := make([]string, len(instances))\n\tfor i, inst := range instances {\n\t\tresult[i] = inst.ID()\n\t}\n\n\tsort.Strings(result)\n\treturn result\n\n}\n\nfunc TestGroupings(t *testing.T) {\n\ttests := []struct {\n\t\tlabel string\n\t\tgroup grp.InstanceGroup\n\t\twants []string\n\t}{\n\t\t{\"cluster\", grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"foo-crit\"), []string{\"i-11111111\", \"i-22222222\"}},\n\t\t{\"stack\", grp.New(\"foo\", \"prod\", \"us-east-1\", \"staging\", \"\"), []string{\"i-55555555\", \"i-66666666\", \"i-77777777\", \"i-88888888\"}},\n\t\t{\"app\", grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\"), []string{\"i-11111111\", \"i-22222222\", \"i-33333333\", \"i-44444444\", \"i-55555555\", \"i-66666666\", \"i-77777777\", \"i-88888888\"}},\n\t\t{\"cluster, all regions\", grp.New(\"foo\", \"prod\", \"\", \"\", \"foo-crit\"), []string{\"i-11111111\", \"i-22222222\", \"i-aaaaaaaa\", \"i-bbbbbbbb\"}},\n\t\t{\"stack, all regions\", grp.New(\"foo\", \"prod\", \"\", \"staging\", \"\"), []string{\"i-55555555\", \"i-66666666\", \"i-77777777\", \"i-88888888\", \"i-cccccccc\", \"i-dddddddd\"}},\n\t\t{\"app, all regions\", grp.New(\"foo\", \"prod\", \"\", \"\", \"\"), []string{\"i-11111111\", \"i-22222222\", \"i-33333333\", \"i-44444444\", \"i-55555555\", \"i-66666666\", \"i-77777777\", \"i-88888888\", \"i-aaaaaaaa\", \"i-bbbbbbbb\", \"i-cccccccc\", \"i-dddddddd\"}},\n\t}\n\n\t// setup\n\tdep := mockDeployment()\n\n\tfor _, tt := range tests {\n\t\tinstances, err := Instances(tt.group, nil, dep)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%+v\", err)\n\t\t}\n\n\t\t// assertions\n\t\tgots := ids(instances)\n\n\t\tif got, want := len(gots), len(tt.wants); got != want {\n\t\t\tt.Errorf(\"%s: len(eligible.Instances(group, cfg, app))=%v, want %v\", tt.label, got, want)\n\t\t\tcontinue\n\t\t}\n\n\t\tfor i, got := range gots {\n\t\t\tif want := tt.wants[i]; got != want {\n\t\t\t\tt.Errorf(\"%s: got=%v, want=%v\", tt.label, got, want)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestAppLevelGroupingWhereClustersAreRegionSpecific(t *testing.T) {\n\tdep := &mock.Deployment{AppMap: map[string]D.AppMap{\n\t\t\"foo\": {\"prod\": D.AccountInfo{CloudProvider: \"aws\", Clusters: D.ClusterMap{\n\t\t\t\"foo-useast1\": {\n\t\t\t\t\"us-east-1\": {\"foo-useast1-v001\": []D.InstanceID{\"i-11111111\", \"i-22222222\", \"i-33333333\"}},\n\t\t\t},\n\t\t\t\"foo-uswest2\": {\n\t\t\t\t\"us-west-2\": {\"foo-uswest2-v005\": []D.InstanceID{\"i-cccccccc\", \"i-dddddddd\"}},\n\t\t\t},\n\t\t}},\n\t\t}}}\n\n\tgroup := grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\")\n\n\tinstances, err := Instances(group, nil, dep)\n\tif err != nil {\n\t\tt.Fatalf(\"%+v\", err)\n\t}\n\n\tif got, want := len(instances), 3; got != want {\n\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t}\n}\n\nfunc TestAppLevelGroupingWhereClusterIsInTwoRegions(t *testing.T) {\n\tdep := &mock.Deployment{AppMap: map[string]D.AppMap{\n\t\t\"foo\": {\"prod\": D.AccountInfo{CloudProvider: \"aws\", Clusters: D.ClusterMap{\n\t\t\t\"foo-prod\": {\n\t\t\t\t\"us-east-1\": {\"foo-prod-v001\": []D.InstanceID{\"i-11111111\", \"i-22222222\", \"i-33333333\"}},\n\t\t\t\t\"us-west-2\": {\"foo-prod-v001\": []D.InstanceID{\"i-aaaaaaaa\", \"i-bbbbbbbb\", \"i-cccccccc\"}},\n\t\t\t},\n\t\t}}}}}\n\n\tgroup := grp.New(\"foo\", \"prod\", \"\", \"\", \"\")\n\n\tinstances, err := Instances(group, nil, dep)\n\tif err != nil {\n\t\tt.Fatalf(\"%+v\", err)\n\t}\n\n\tif got, want := len(instances), 6; got != want {\n\t\tt.Errorf(\"got: %d, want: %d\", got, want)\n\t}\n}\n\nfunc TestExceptions(t *testing.T) {\n\ttests := []struct {\n\t\tlabel string\n\t\texs   []chaosmonkey.Exception\n\t\twants []string\n\t}{\n\t\t{\"stack/detail/region\", []chaosmonkey.Exception{{Account: \"prod\", Stack: \"crit\", Detail: \"lorin\", Region: \"us-east-1\"}}, []string{\"i-11111111\", \"i-22222222\", \"i-55555555\", \"i-66666666\", \"i-77777777\", \"i-88888888\"}},\n\t\t{\"stack/detail\", []chaosmonkey.Exception{{Account: \"prod\", Stack: \"crit\", Detail: \"lorin\", Region: \"*\"}}, []string{\"i-11111111\", \"i-22222222\", \"i-55555555\", \"i-66666666\", \"i-77777777\", \"i-88888888\"}},\n\t\t{\"stack\", []chaosmonkey.Exception{{Account: \"prod\", Stack: \"crit\", Detail: \"*\", Region: \"*\"}}, []string{\"i-55555555\", \"i-66666666\", \"i-77777777\", \"i-88888888\"}},\n\t\t{\"detail\", []chaosmonkey.Exception{{Account: \"prod\", Stack: \"*\", Detail: \"lorin\", Region: \"*\"}}, []string{\"i-11111111\", \"i-22222222\", \"i-55555555\", \"i-66666666\"}},\n\t\t{\"all stacks\", []chaosmonkey.Exception{{Account: \"prod\", Stack: \"crit\", Detail: \"*\", Region: \"*\"}, {Account: \"prod\", Stack: \"staging\", Detail: \"*\", Region: \"*\"}}, nil},\n\t\t{\"blank stack\", []chaosmonkey.Exception{{Account: \"prod\", Stack: \"*\", Detail: \"\", Region: \"*\"}}, []string{\"i-33333333\", \"i-44444444\", \"i-77777777\", \"i-88888888\"}},\n\t\t{\"stack, detail\", []chaosmonkey.Exception{{Account: \"prod\", Stack: \"crit\", Detail: \"*\", Region: \"*\"}, {Account: \"prod\", Stack: \"*\", Detail: \"lorin\", Region: \"*\"}}, []string{\"i-55555555\", \"i-66666666\"}},\n\t}\n\n\t// setup\n\tgroup := grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\")\n\tdep := mockDeployment()\n\n\tfor _, tt := range tests {\n\t\tinstances, err := Instances(group, tt.exs, dep)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%+v\", err)\n\t\t}\n\n\t\t// assertions\n\t\tgots := ids(instances)\n\n\t\tif got, want := len(gots), len(tt.wants); got != want {\n\t\t\tt.Errorf(\"%s: len(eligible.Instances(group, cfg, app))=%v, want %v\", tt.label, got, want)\n\t\t\tcontinue\n\t\t}\n\n\t\tfor i, got := range gots {\n\t\t\tif want := tt.wants[i]; got != want {\n\t\t\t\tt.Errorf(\"%s: got=%v, want=%v\", tt.label, got, want)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "eligible/instances_canary_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage eligible\n\nimport (\n\t\"testing\"\n\n\tD \"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n\t\"github.com/Netflix/chaosmonkey/v2/mock\"\n)\n\n// Test that canaries are not considered eligible instances\nfunc TestNoKillCanaries(t *testing.T) {\n\tusEast1 := D.RegionName(\"us-east-1\")\n\tusWest2 := D.RegionName(\"us-west-2\")\n\n\tdep := mock.NewDeployment(\n\t\tmap[string]D.AppMap{\n\t\t\t\"mock\": {\n\t\t\t\tD.AccountName(\"prod\"): {\n\t\t\t\t\tCloudProvider: \"aws\",\n\t\t\t\t\tClusters: D.ClusterMap{\n\t\t\t\t\t\tD.ClusterName(\"mock-prod-a\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-a-v123\"): []D.InstanceID{\"i-4a003cd0\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-a-v111\"): []D.InstanceID{\"i-efdc42dc\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"mock-prod-b\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-v002\"): []D.InstanceID{\"i-115ccc27\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-v001\"): []D.InstanceID{\"i-7881287e\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"mock-prod-b-baseline\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-baseline-v012\"): []D.InstanceID{\"i-e71a94d0\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-baseline-v011\"): []D.InstanceID{\"i-69211000\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"mock-prod-b-canary\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-canary-v012\"): []D.InstanceID{\"i-18d2e1b6\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-canary-v011\"): []D.InstanceID{\"i-63bda865\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"mock-prod-a-citrus\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-citrus-v014\"): []D.InstanceID{\"i-d26e6af1\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-citrus-v013\"): []D.InstanceID{\"i-1db216c3\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"mock-prod-a-citrusproxy\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-citrusproxy-v020\"): []D.InstanceID{\"i-c57ad10c\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-citrusproxy-v017\"): []D.InstanceID{\"i-6fba090b\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n\n\t// Group is all instances in mock app, prod group\n\tgroup := grp.New(\"mock\", \"prod\", \"\", \"\", \"\")\n\tinstances, err := Instances(group, nil, dep)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tgot, want := len(instances), 4\n\tif got != want {\n\t\tt.Fatalf(\"len(EligibleInstances(group, cfg, deployInfo))=%d, want %d\", got, want)\n\t}\n}\n"
  },
  {
    "path": "eligible/instances_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage eligible\n\nimport (\n\t\"testing\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\tD \"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n\t\"github.com/Netflix/chaosmonkey/v2/mock\"\n)\n\n// mockDeployment returns a deploy.Deployment object mock for testing\nfunc mockDep() D.Deployment {\n\tusEast1 := D.RegionName(\"us-east-1\")\n\tusWest2 := D.RegionName(\"us-west-2\")\n\treturn mock.NewDeployment(\n\t\tmap[string]D.AppMap{\n\t\t\t\"mock\": {\n\t\t\t\tD.AccountName(\"prod\"): {\n\t\t\t\t\tCloudProvider: \"aws\",\n\t\t\t\t\tClusters: D.ClusterMap{\n\t\t\t\t\t\tD.ClusterName(\"mock-prod-a\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-a-v123\"): []D.InstanceID{\"i-4a003cd0\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-a-v111\"): []D.InstanceID{\"i-efdc42dc\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"mock-prod-b\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-v002\"): []D.InstanceID{\"i-115ccc27\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-prod-b-v001\"): []D.InstanceID{\"i-7881287e\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"mock-staging-a\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-staging-a-v123\"): []D.InstanceID{\"i-ff8e7e4b\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-staging-a-v111\"): []D.InstanceID{\"i-6eed18a4\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"mock-staging-b\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-staging-b-v002\"): []D.InstanceID{\"i-13770e40\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-staging-b-v001\"): []D.InstanceID{\"i-afb7595e\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tD.AccountName(\"test\"): {\n\t\t\t\t\tCloudProvider: \"aws\",\n\t\t\t\t\tClusters: D.ClusterMap{\n\t\t\t\t\t\tD.ClusterName(\"mock-test-a\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-test-a-v123\"): []D.InstanceID{\"i-23b61f89\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-test-a-v111\"): []D.InstanceID{\"i-fe7a0827\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"mock-test-b\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-test-b-v002\"): []D.InstanceID{\"i-f581d5c3\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-test-b-v001\"): []D.InstanceID{\"i-986e988a\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"mock-beta-a\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-beta-a-v123\"): []D.InstanceID{\"i-4b359d5d\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-beta-a-v111\"): []D.InstanceID{\"i-e751bdd2\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"mock-beta-b\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-beta-b-v002\"): []D.InstanceID{\"i-e5eeba5e\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"mock-beta-b-v001\"): []D.InstanceID{\"i-76013ffb\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}})\n}\n\nfunc TestInstances(t *testing.T) {\n\tdep := mockDep()\n\tgroup := grp.New(\"mock\", \"prod\", \"us-east-1\", \"\", \"mock-prod-a\")\n\n\tinstances, err := Instances(group, nil, dep)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tgot, want := len(instances), 1\n\tif got != want {\n\t\tt.Fatalf(\"len(Instances(group, nil, dep))=%v, want %v\", got, want)\n\t}\n\n\tif instances[0].ID() != \"i-4a003cd0\" {\n\t\tt.Fatal(\"Expected id i-4a003cd0, got\", instances[0].ID())\n\t}\n}\n\nfunc TestSimpleException(t *testing.T) {\n\tdep := mockDep()\n\tgroup := grp.New(\"mock\", \"prod\", \"us-east-1\", \"\", \"mock-prod-a\")\n\texs := []chaosmonkey.Exception{{Account: \"prod\", Stack: \"prod\", Detail: \"a\", Region: \"us-east-1\"}}\n\tinstances, err := Instances(group, exs, dep)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tgot, want := len(instances), 0\n\tif got != want {\n\t\tt.Fatalf(\"len(Instances(group, exs, dep))=%v, want %v\", got, want)\n\t}\n}\n\nfunc TestMultipleExceptions(t *testing.T) {\n\tapp := abcloudMockDep()\n\t// Group across everything in prod\n\tgroup := grp.New(\"abcloud\", \"prod\", \"\", \"\", \"\")\n\texs := []chaosmonkey.Exception{\n\t\t{Account: \"prod\", Stack: \"batch\", Detail: \"\", Region: \"eu-west-1\"},\n\t\t{Account: \"prod\", Stack: \"ecom\", Detail: \"\", Region: \"us-west-2\"},\n\t\t{Account: \"prod\", Stack: \"\", Detail: \"\", Region: \"us-west-2\"},\n\t}\n\n\tinstances, err := Instances(group, exs, app)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tgot, want := len(instances), 6\n\tif got != want {\n\t\tt.Fatalf(\"len(Instances(group, cfg, app))=%v, want %v\", got, want)\n\t}\n\n\t// Ensure none of the excepted instances are in the list\n\tfor _, instance := range instances {\n\t\tif instance.ID() == \"i-8a1bd7ac\" || instance.ID() == \"i-2910a0e4\" || instance.ID() == \"i-b28a69c8\" {\n\t\t\tt.Errorf(\"excepted instance is present: %v\", instance)\n\t\t}\n\t}\n}\n\n// mockDep based on actual structure of abcloud\nfunc abcloudMockDep() D.Deployment {\n\tusEast1 := D.RegionName(\"us-east-1\")\n\tusWest2 := D.RegionName(\"us-west-2\")\n\teuWest1 := D.RegionName(\"eu-west-1\")\n\treturn mock.NewDeployment(\n\t\tmap[string]D.AppMap{\n\t\t\t\"abcloud\": {\n\t\t\t\tD.AccountName(\"prod\"): {\n\t\t\t\t\tCloudProvider: \"aws\",\n\t\t\t\t\tClusters: D.ClusterMap{\n\t\t\t\t\t\tD.ClusterName(\"abcloud\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"abcloud-v123\"): []D.InstanceID{\"i-7921a2f8\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"abcloud-v123\"): []D.InstanceID{\"i-8a1bd7ac\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\teuWest1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"abcloud-v123\"): []D.InstanceID{\"i-87a90e92\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"abcloud-batch\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"abcloud-batch-v123\"): []D.InstanceID{\"i-2c25ab60\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"abcloud-batch-v123\"): []D.InstanceID{\"i-3bc40bdb\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\teuWest1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"abcloud-batch-v123\"): []D.InstanceID{\"i-2910a0e4\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tD.ClusterName(\"abcloud-ecom\"): {\n\t\t\t\t\t\t\tusEast1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"abcloud-ecom-v123\"): []D.InstanceID{\"i-ab9a4f10\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tusWest2: {\n\t\t\t\t\t\t\t\tD.ASGName(\"abcloud-ecom-v123\"): []D.InstanceID{\"i-b28a69c8\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\teuWest1: {\n\t\t\t\t\t\t\t\tD.ASGName(\"abcloud-ecom-v123\"): []D.InstanceID{\"i-4fa09365\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "env/env.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package env contains a no-op implementation of chaosmonkey.env\n// where InTest() always returns false\npackage env\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n)\n\n// notTestEnv is an environment that does not report as a test env\ntype notTestEnv struct{}\n\n// InTest implements chaosmonkey.Env.InTest\nfunc (n notTestEnv) InTest() bool {\n\treturn false\n}\n\nfunc init() {\n\tdeps.GetEnv = getNotTestEnv\n}\n\nfunc getNotTestEnv(cfg *config.Monkey) (chaosmonkey.Env, error) {\n\treturn notTestEnv{}, nil\n}\n"
  },
  {
    "path": "errorcounter/errorcounter.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage errorcounter\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n\t\"github.com/pkg/errors\"\n)\n\n// Netflix uses Atlas for tracking error events.\n// In the open-source build, we currently only support a null (no-op) error\n// counter\n\ntype nullErrorCounter struct{}\n\nfunc (n nullErrorCounter) Increment() error {\n\treturn nil\n}\n\nfunc init() {\n\tdeps.GetErrorCounter = getNullErrorCounter\n}\n\nfunc getNullErrorCounter(cfg *config.Monkey) (chaosmonkey.ErrorCounter, error) {\n\tkind := cfg.ErrorCounter()\n\tif kind != \"\" {\n\t\treturn nil, errors.Errorf(\"unsupported error counter: %s\", kind)\n\t}\n\n\treturn nullErrorCounter{}, nil\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/Netflix/chaosmonkey/v2\n\ngo 1.19\n\nrequire (\n\tgithub.com/SmartThingsOSS/frigga-go v0.0.0-20180827230714-55b2c36db3e7\n\tgithub.com/davecgh/go-spew v1.1.1\n\tgithub.com/go-sql-driver/mysql v1.2.1-0.20160802113842-0b58b37b664c\n\tgithub.com/kardianos/osext v0.0.0-20160811001526-c2c54e542fb7\n\tgithub.com/pkg/errors v0.7.2-0.20160916110212-a887431f7f6e\n\tgithub.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362\n\tgithub.com/spf13/pflag v0.0.0-20160915153101-c7e63cf4530b\n\tgithub.com/spf13/viper v0.0.0-20160926150402-382f87b929b8\n\tgolang.org/x/crypto v0.0.0-20160922170629-8e06e8ddd962\n)\n\nrequire (\n\tgithub.com/fsnotify/fsnotify v1.3.2-0.20160816051541-f12c6236fe7b // indirect\n\tgithub.com/hashicorp/hcl v0.0.0-20160916130100-ef8133da8cda // indirect\n\tgithub.com/kr/fs v0.0.0-20131111012553-2788f0dbd169 // indirect\n\tgithub.com/lib/pq v1.10.7 // indirect\n\tgithub.com/magiconair/properties v1.7.1-0.20160908093658-0723e352fa35 // indirect\n\tgithub.com/mattn/go-sqlite3 v1.14.16 // indirect\n\tgithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee // indirect\n\tgithub.com/pelletier/go-buffruneio v0.1.0 // indirect\n\tgithub.com/pelletier/go-toml v0.3.6-0.20160920070715-45932ad32dfd // indirect\n\tgithub.com/pkg/sftp v0.0.0-20160908100035-8197a2e58073 // indirect\n\tgithub.com/spf13/afero v0.0.0-20160919210114-52e4a6cfac46 // indirect\n\tgithub.com/spf13/cast v0.0.0-20160926084249-2580bc98dc0e // indirect\n\tgithub.com/spf13/jwalterweatherman v0.0.0-20160311093646-33c24e77fb80 // indirect\n\tgithub.com/stretchr/testify v1.8.1 // indirect\n\tgithub.com/ziutek/mymysql v1.5.4 // indirect\n\tgolang.org/x/sys v0.0.0-20160916181909-8f0908ab3b24 // indirect\n\tgolang.org/x/text v0.9.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/gorp.v1 v1.7.1 // indirect\n\tgopkg.in/yaml.v2 v2.0.0-20160912165603-31c299268d30 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/SmartThingsOSS/frigga-go v0.0.0-20180827230714-55b2c36db3e7 h1:e3ZaLXEVpiXqp5D/ozG2C6ahR8IctL6TsPrVQN8gbws=\ngithub.com/SmartThingsOSS/frigga-go v0.0.0-20180827230714-55b2c36db3e7/go.mod h1:zvIvIUsOj4xScRxxSFfHpGwBAf5QtsUm/L8CMUC24DY=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fsnotify/fsnotify v1.3.2-0.20160816051541-f12c6236fe7b h1:clQtr7BsnoijdumdhlbbOGglPb1lIAJ3yTPjYOHlKdQ=\ngithub.com/fsnotify/fsnotify v1.3.2-0.20160816051541-f12c6236fe7b/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/go-sql-driver/mysql v1.2.1-0.20160802113842-0b58b37b664c h1:QD/OSWIQcR3PMs9GzsjN5QOVvxvDI+WrK0GbvNapPds=\ngithub.com/go-sql-driver/mysql v1.2.1-0.20160802113842-0b58b37b664c/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/hashicorp/hcl v0.0.0-20160916130100-ef8133da8cda h1:itWS1A5qekCk9zuBVRDiUE2Zmg25Wgp08tQP/Xcv5KE=\ngithub.com/hashicorp/hcl v0.0.0-20160916130100-ef8133da8cda/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=\ngithub.com/kardianos/osext v0.0.0-20160811001526-c2c54e542fb7 h1:pKv4oHt3kat9yf1jofmaRv3KxGaY5B7VV55GrfXFa74=\ngithub.com/kardianos/osext v0.0.0-20160811001526-c2c54e542fb7/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=\ngithub.com/kr/fs v0.0.0-20131111012553-2788f0dbd169 h1:YUrU1/jxRqnt0PSrKj1Uj/wEjk/fjnE80QFfi2Zlj7Q=\ngithub.com/kr/fs v0.0.0-20131111012553-2788f0dbd169/go.mod h1:glhvuHOU9Hy7/8PwwdtnarXqLagOX0b/TbZx2zLMqEg=\ngithub.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=\ngithub.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/magiconair/properties v1.7.1-0.20160908093658-0723e352fa35 h1:WtkHGe1cgg+lvDj9p5CvjXrfopsIss0vIAz+/zeYZyQ=\ngithub.com/magiconair/properties v1.7.1-0.20160908093658-0723e352fa35/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=\ngithub.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee h1:kK7VuFVykgt0LfMSloWYjDOt4TnOcL0AxF0/rDq2VkM=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/pelletier/go-buffruneio v0.1.0 h1:ig6N9Cg71k/P+UUbhwdOFtJWz+qa8/3by7AzMprMWBM=\ngithub.com/pelletier/go-buffruneio v0.1.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=\ngithub.com/pelletier/go-toml v0.3.6-0.20160920070715-45932ad32dfd h1:LFdCPBzgbbt6CoebmOy/ePk3eeHgoJRh9RhQVGe2itk=\ngithub.com/pelletier/go-toml v0.3.6-0.20160920070715-45932ad32dfd/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pkg/errors v0.7.2-0.20160916110212-a887431f7f6e h1:X0D2BP2MR4Z7k6pAv4RRKm7/QiWHyQ65lemqHxZhTus=\ngithub.com/pkg/errors v0.7.2-0.20160916110212-a887431f7f6e/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v0.0.0-20160908100035-8197a2e58073 h1:9PqYQCzKEbilrPJl3LDO16HdbA25Yqc3I25aUfgFaCs=\ngithub.com/pkg/sftp v0.0.0-20160908100035-8197a2e58073/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362 h1:lmOdpLt3XS6QyVoY6xNfOOTNWE2xtUBees+OAO+HFOg=\ngithub.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=\ngithub.com/spf13/afero v0.0.0-20160919210114-52e4a6cfac46 h1:oJAUI67mq3xuqudgt8CGd+pkKPML8+AoFWzP1vPYHFc=\ngithub.com/spf13/afero v0.0.0-20160919210114-52e4a6cfac46/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/cast v0.0.0-20160926084249-2580bc98dc0e h1:+axhEi83O3FFcwP/e9t09UHRmV1zZFl8RsgtO0zuZhY=\ngithub.com/spf13/cast v0.0.0-20160926084249-2580bc98dc0e/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=\ngithub.com/spf13/jwalterweatherman v0.0.0-20160311093646-33c24e77fb80 h1:evyGXhHMrxKBDkdlSPv9HMWV2o53o+Ibhm28BGc0450=\ngithub.com/spf13/jwalterweatherman v0.0.0-20160311093646-33c24e77fb80/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/pflag v0.0.0-20160915153101-c7e63cf4530b h1:wT0f1lvMzot+G0vEQQqBBJIHEj5l+fVx72f7BC9xU14=\ngithub.com/spf13/pflag v0.0.0-20160915153101-c7e63cf4530b/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/viper v0.0.0-20160926150402-382f87b929b8 h1:A8AWhlFmNTRnefa19v+fHaB1KkyQv7J89B5YUWQvbWE=\ngithub.com/spf13/viper v0.0.0-20160926150402-382f87b929b8/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=\ngithub.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=\ngolang.org/x/crypto v0.0.0-20160922170629-8e06e8ddd962 h1:mWFWs/KZ0R2cLgNYRn+C4pQvMDmpkqxF1Npt3NEAPg0=\ngolang.org/x/crypto v0.0.0-20160922170629-8e06e8ddd962/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/sys v0.0.0-20160916181909-8f0908ab3b24 h1:BL/wcoHkjubwHn2wDTAD1fehKZ9lf67KOVzucRKWPtM=\ngolang.org/x/sys v0.0.0-20160916181909-8f0908ab3b24/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/text v0.0.0-20160922232553-a7c023693a94 h1:QmdGgXvDlzDdp+l90rsC+qLFmFhl2nn+rSfBnS8P4zI=\ngolang.org/x/text v0.0.0-20160922232553-a7c023693a94/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/gorp.v1 v1.7.1 h1:GBB9KrWRATQZh95HJyVGUZrWwOPswitEYEyqlK8JbAA=\ngopkg.in/gorp.v1 v1.7.1/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=\ngopkg.in/yaml.v2 v2.0.0-20160912165603-31c299268d30 h1:mNnzt76aN10kG6/XNojKVKR8VDIWvEp4mlj5kyRf6hk=\ngopkg.in/yaml.v2 v2.0.0-20160912165603-31c299268d30/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "grp/grp.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package grp holds the InstanceGroup interface\npackage grp\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/SmartThingsOSS/frigga-go\"\n\t\"log\"\n)\n\n// New generates an InstanceGroup.\n// region, stack, and cluster may be empty strings, in which case\n// the group is cross-region, cross-stack, or cross-cluster\n// Note that stack and cluster are mutually exclusive, can specify one\n// but not both\nfunc New(app, account, region, stack, cluster string) InstanceGroup {\n\treturn group{\n\t\tapp:     app,\n\t\taccount: account,\n\t\tregion:  region,\n\t\tstack:   stack,\n\t\tcluster: cluster,\n\t}\n}\n\n// InstanceGroup represents a group of instances\ntype InstanceGroup interface {\n\t// App returns the name of the app\n\tApp() string\n\n\t// Account returns the name of the account\n\tAccount() string\n\n\t// Region returns (region name, region present)\n\t// If the group is cross-region, the boolean will be false\n\tRegion() (name string, ok bool)\n\n\t// Stack returns (region name, region present)\n\t// If the group is cross-stack, the boolean will be false\n\tStack() (name string, ok bool)\n\n\t// Cluster returns (cluster name, cluster present)\n\t// If the group is cross-cluster, the boolean will be false\n\tCluster() (name string, ok bool)\n\n\t// String outputs a stringified rep\n\tString() string\n}\n\n// Equal returns true if g1 and g2 represent the same group of instances\nfunc Equal(g1, g2 InstanceGroup) bool {\n\tif g1 == g2 {\n\t\treturn true\n\t}\n\n\tif g1.App() != g2.App() {\n\t\treturn false\n\t}\n\n\tif g1.Account() != g2.Account() {\n\t\treturn false\n\t}\n\n\tr1, ok1 := g1.Region()\n\tr2, ok2 := g2.Region()\n\tif ok1 != ok2 {\n\t\treturn false\n\t}\n\n\tif ok1 && (r1 != r2) {\n\t\treturn false\n\t}\n\n\ts1, ok1 := g1.Stack()\n\ts2, ok2 := g2.Stack()\n\n\tif ok1 != ok2 {\n\t\treturn false\n\t}\n\n\tif ok1 && (s1 != s2) {\n\t\treturn false\n\t}\n\n\tc1, ok1 := g1.Cluster()\n\tc2, ok2 := g2.Cluster()\n\n\tif ok1 != ok2 {\n\t\treturn false\n\t}\n\n\tif ok1 && (c1 != c2) {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// String outputs a string representation of InstanceGroup suitable for logging\nfunc String(group InstanceGroup) string {\n\tvar buffer bytes.Buffer\n\twriteString := func(s string) {\n\t\t_, _ = buffer.WriteString(s)\n\t}\n\twriteString(\"app=\")\n\twriteString(group.App())\n\twriteString(\" account=\")\n\twriteString(group.Account())\n\tregion, ok := group.Region()\n\tif ok {\n\t\twriteString(\" region=\")\n\t\twriteString(region)\n\t}\n\tstack, ok := group.Stack()\n\tif ok {\n\t\twriteString(\" stack=\")\n\t\twriteString(stack)\n\t}\n\tcluster, ok := group.Cluster()\n\tif ok {\n\t\twriteString(\" cluster=\")\n\t\twriteString(cluster)\n\t}\n\n\treturn buffer.String()\n}\n\ntype group struct {\n\tapp, account, region, stack, cluster string\n}\n\nfunc (g group) String() string {\n\treturn fmt.Sprintf(\"InstanceGroup{app=%s account=%s region=%s stack=%s cluster=%s}\", g.app, g.account, g.region, g.stack, g.cluster)\n}\n\nfunc (g group) MarshalJSON() ([]byte, error) {\n\tvar s = struct {\n\t\tApp     string `json:\"app\"`\n\t\tAccount string `json:\"account\"`\n\t\tRegion  string `json:\"region,omitempty\"`\n\t\tStack   string `json:\"stack,omitempty\"`\n\t\tCluster string `json:\"cluster,omitempty\"`\n\t}{\n\t\tApp:     g.app,\n\t\tAccount: g.account,\n\t\tRegion:  g.region,\n\t\tStack:   g.stack,\n\t\tCluster: g.cluster,\n\t}\n\n\treturn json.Marshal(s)\n}\n\n// App implements InstanceGroup.App\nfunc (g group) App() string {\n\treturn g.app\n}\n\n// Account implements InstanceGroup.Account\nfunc (g group) Account() string {\n\treturn g.account\n}\n\n// Region implements InstanceGroup.Region\nfunc (g group) Region() (string, bool) {\n\tif g.region == \"\" {\n\t\treturn \"\", false\n\t}\n\treturn g.region, true\n}\n\n// Stack implements InstanceGroup.Stack\nfunc (g group) Stack() (string, bool) {\n\tif g.stack == \"\" {\n\t\treturn \"\", false\n\t}\n\treturn g.stack, true\n}\n\n// Cluster implements InstanceGroup.Cluster\nfunc (g group) Cluster() (string, bool) {\n\tif g.cluster == \"\" {\n\t\treturn \"\", false\n\t}\n\treturn g.cluster, true\n}\n\n// AnyRegion is true if the group matches any region\nfunc AnyRegion(g InstanceGroup) bool {\n\t_, specific := g.Region()\n\treturn !specific\n}\n\n// AnyStack is true if the group matches any stack\nfunc AnyStack(g InstanceGroup) bool {\n\t_, specific := g.Stack()\n\treturn !specific\n}\n\n// AnyCluster is true if the group matches any cluster\nfunc AnyCluster(g InstanceGroup) bool {\n\t_, specific := g.Cluster()\n\treturn !specific\n}\n\n// Contains returns true if the (account, region, cluster) is within the instance group\nfunc Contains(g InstanceGroup, account, region, cluster string) bool {\n\tnames, err := frigga.Parse(cluster)\n\tif err != nil {\n\t\tlog.Printf(\"WARNING: could not parse cluster name: %s\", cluster)\n\t\treturn false\n\t}\n\n\treturn names.App == g.App() &&\n\t\tstring(account) == g.Account() &&\n\t\t(AnyRegion(g) || string(region) == must(g.Region())) &&\n\t\t(AnyStack(g) || names.Stack == must(g.Stack())) &&\n\t\t(AnyCluster(g) || string(cluster) == must(g.Cluster()))\n}\n\n// must returns val if ok is true\n// panics otherwise\nfunc must(val string, specific bool) string {\n\tif !specific {\n\t\tpanic(\"specific was unexpectedly false\")\n\t}\n\treturn val\n}\n"
  },
  {
    "path": "grp/grp_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage grp_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n)\n\nfunc TestNewAppWithRegion(t *testing.T) {\n\tgroup := grp.New(\"myapp\", \"prod\", \"us-east-1\", \"\", \"\")\n\n\tif group.App() != \"myapp\" {\n\t\tt.Error(\"Expected myapp, got\", group.App())\n\t}\n\n\tif group.Account() != \"prod\" {\n\t\tt.Error(\"Expected prod, got\", group.Account())\n\t}\n\n\tregion, ok := group.Region()\n\tif !ok || region != \"us-east-1\" {\n\t\tt.Error(\"Expected us-east-1\")\n\t}\n\n\tif _, ok := group.Stack(); ok {\n\t\tt.Error(\"Expected no stack\")\n\t}\n\n\tif _, ok := group.Cluster(); ok {\n\t\tt.Error(\"Expected no cluster\")\n\t}\n}\n\nfunc TestNewAppCrossRegion(t *testing.T) {\n\tgroup := grp.New(\"myapp\", \"prod\", \"\", \"\", \"\")\n\n\tif group.App() != \"myapp\" {\n\t\tt.Error(\"Expected myapp, got\", group.App())\n\t}\n\n\tif group.Account() != \"prod\" {\n\t\tt.Error(\"Expected prod, got\", group.Account())\n\t}\n\n\tif _, ok := group.Region(); ok {\n\t\tt.Error(\"Expected no region\")\n\t}\n\n\tif _, ok := group.Stack(); ok {\n\t\tt.Error(\"Expected no stack\")\n\t}\n\n\tif _, ok := group.Cluster(); ok {\n\t\tt.Error(\"Expected no cluster\")\n\t}\n}\n\nfunc TestNewStackWithRegion(t *testing.T) {\n\tgroup := grp.New(\"myapp\", \"prod\", \"us-east-1\", \"staging\", \"\")\n\n\tif group.App() != \"myapp\" {\n\t\tt.Error(\"Expected myapp, got\", group.App())\n\t}\n\n\tif group.Account() != \"prod\" {\n\t\tt.Error(\"Expected prod, got\", group.Account())\n\t}\n\n\tregion, ok := group.Region()\n\tif !ok || region != \"us-east-1\" {\n\t\tt.Error(\"Expected us-east-1\")\n\t}\n\n\tstack, ok := group.Stack()\n\tif !ok || stack != \"staging\" {\n\t\tt.Error(\"Expected stack=staging, got stack=\", stack)\n\t}\n\n\tif _, ok := group.Cluster(); ok {\n\t\tt.Error(\"Expected no cluster\")\n\t}\n}\n\nfunc TestNewStackCrossRegion(t *testing.T) {\n\tgroup := grp.New(\"myapp\", \"prod\", \"\", \"staging\", \"\")\n\n\tif group.App() != \"myapp\" {\n\t\tt.Error(\"Expected myapp, got\", group.App())\n\t}\n\n\tif group.Account() != \"prod\" {\n\t\tt.Error(\"Expected prod, got\", group.Account())\n\t}\n\n\tif _, ok := group.Region(); ok {\n\t\tt.Error(\"Expected no region\")\n\t}\n\n\tstack, ok := group.Stack()\n\tif !ok || stack != \"staging\" {\n\t\tt.Error(\"Expected stack=staging, got stack=\", stack)\n\t}\n\n\tif _, ok := group.Cluster(); ok {\n\t\tt.Error(\"Expected no cluster\")\n\t}\n}\n\nfunc TestNewClusterWithRegion(t *testing.T) {\n\tgroup := grp.New(\"myapp\", \"prod\", \"us-east-1\", \"\", \"myapp-prod-foo\")\n\n\tif group.App() != \"myapp\" {\n\t\tt.Error(\"Expected myapp, got\", group.App())\n\t}\n\n\tif group.Account() != \"prod\" {\n\t\tt.Error(\"Expected prod, got\", group.Account())\n\t}\n\n\tregion, ok := group.Region()\n\tif !ok || region != \"us-east-1\" {\n\t\tt.Error(\"Expected us-east-1\")\n\t}\n\n\tif _, ok := group.Stack(); ok {\n\t\tt.Error(\"Expected no stack\")\n\t}\n\n\tcluster, ok := group.Cluster()\n\tif !ok || cluster != \"myapp-prod-foo\" {\n\t\tt.Error(\"Expected cluster myapp-prod-foo, got\", cluster)\n\t}\n}\n\nfunc TestNewClusterCrossRegion(t *testing.T) {\n\tgroup := grp.New(\"myapp\", \"prod\", \"\", \"\", \"myapp-prod-foo\")\n\n\tif group.App() != \"myapp\" {\n\t\tt.Error(\"Expected myapp, got\", group.App())\n\t}\n\n\tif group.Account() != \"prod\" {\n\t\tt.Error(\"Expected prod, got\", group.Account())\n\t}\n\n\tif _, ok := group.Region(); ok {\n\t\tt.Error(\"Expected no region\")\n\t}\n\n\tif _, ok := group.Stack(); ok {\n\t\tt.Error(\"Expected no stack\")\n\t}\n\n\tcluster, ok := group.Cluster()\n\tif !ok || cluster != \"myapp-prod-foo\" {\n\t\tt.Error(\"Expected cluster myapp-prod-foo, got\", cluster)\n\t}\n}\n\nfunc TestContains(t *testing.T) {\n\ttests := []struct {\n\t\tgroup                    grp.InstanceGroup\n\t\taccount, region, cluster string\n\t\tmatches                  bool\n\t}{\n\t\t{grp.New(\"foo\", \"prod\", \"\", \"\", \"\"), \"prod\", \"us-east-1\", \"foo-staging-a\", true},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\"), \"prod\", \"us-east-1\", \"foo-staging-a\", true},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\"), \"prod\", \"us-east-1\", \"foo-staging-a\", true},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"foo-staging-a\"), \"prod\", \"us-east-1\", \"foo-staging-a\", true},\n\t\t{grp.New(\"foo\", \"prod\", \"\", \"\", \"\"), \"prod\", \"us-east-1\", \"bar-staging-a\", false},\n\t\t{grp.New(\"foo\", \"prod\", \"\", \"\", \"\"), \"test\", \"us-east-1\", \"foo-staging-a\", false},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"foo-staging-a\"), \"prod\", \"us-west-2\", \"foo-staging-a\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tif grp.Contains(tt.group, tt.account, tt.region, tt.cluster) != tt.matches {\n\t\t\tt.Errorf(\"unexpected grp.Contains(account=%s, region=%s, cluster=%s). group=%+v. expected %t\",\n\t\t\t\ttt.account, tt.region, tt.cluster, tt.group, tt.matches)\n\t\t}\n\t}\n}\n\nfunc TestEqual(t *testing.T) {\n\ttests := []struct {\n\t\tg1   grp.InstanceGroup\n\t\tg2   grp.InstanceGroup\n\t\twant bool\n\t}{\n\t\t{grp.New(\"foo\", \"prod\", \"\", \"\", \"\"), grp.New(\"foo\", \"prod\", \"\", \"\", \"\"), true},\n\t\t{grp.New(\"foo\", \"prod\", \"\", \"\", \"\"), grp.New(\"bar\", \"prod\", \"\", \"\", \"\"), false},\n\t\t{grp.New(\"foo\", \"prod\", \"\", \"\", \"\"), grp.New(\"foo\", \"test\", \"\", \"\", \"\"), false},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\"), grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\"), true},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\"), grp.New(\"foo\", \"prod\", \"us-west-2\", \"\", \"\"), false},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\"), grp.New(\"foo\", \"test\", \"us-east-1\", \"\", \"\"), false},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\"), grp.New(\"bar\", \"prod\", \"us-east-1\", \"\", \"\"), false},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\"), grp.New(\"foo\", \"prod\", \"\", \"\", \"\"), false},\n\t\t{grp.New(\"foo\", \"prod\", \"\", \"\", \"\"), grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"\"), false},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"staging\", \"\"), grp.New(\"foo\", \"prod\", \"us-east-1\", \"staging\", \"\"), true},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"staging\", \"\"), grp.New(\"foo\", \"prod\", \"us-east-1\", \"canary\", \"\"), false},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"staging\", \"\"), grp.New(\"foo\", \"prod\", \"us-west-2\", \"staging\", \"\"), false},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"staging\", \"\"), grp.New(\"bar\", \"prod\", \"us-east-1\", \"staging\", \"\"), false},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"foo-staging-good\"), grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"foo-staging-good\"), true},\n\t\t{grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"foo-staging-good\"), grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"foo-staging-bad\"), false},\n\t\t{grp.New(\"foo\", \"prod\", \"\", \"\", \"foo-staging-good\"), grp.New(\"foo\", \"prod\", \"\", \"\", \"foo-staging-good\"), true},\n\t\t{grp.New(\"foo\", \"prod\", \"\", \"\", \"foo-staging-good\"), grp.New(\"foo\", \"prod\", \"us-east-1\", \"\", \"foo-staging-good\"), false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tif got, want := grp.Equal(tt.g1, tt.g2), tt.want; got != want {\n\t\t\tt.Errorf(\"got Equal(%+v, %+v)=%t, want %t\", tt.g1, tt.g2, got, want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "migration/migrations.go",
    "content": "// Code generated by go-bindata.\n// sources:\n// migration/mysql/1.0.0_initial_schema.sql\n// DO NOT EDIT!\n\npackage migration\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n)\n\nfunc bindataRead(data []byte, name string) ([]byte, error) {\n\tgz, err := gzip.NewReader(bytes.NewBuffer(data))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Read %q: %v\", name, err)\n\t}\n\n\tvar buf bytes.Buffer\n\t_, err = io.Copy(&buf, gz)\n\tclErr := gz.Close()\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Read %q: %v\", name, err)\n\t}\n\tif clErr != nil {\n\t\treturn nil, err\n\t}\n\n\treturn buf.Bytes(), nil\n}\n\ntype asset struct {\n\tbytes []byte\n\tinfo  os.FileInfo\n}\n\ntype bindataFileInfo struct {\n\tname    string\n\tsize    int64\n\tmode    os.FileMode\n\tmodTime time.Time\n}\n\nfunc (fi bindataFileInfo) Name() string {\n\treturn fi.name\n}\nfunc (fi bindataFileInfo) Size() int64 {\n\treturn fi.size\n}\nfunc (fi bindataFileInfo) Mode() os.FileMode {\n\treturn fi.mode\n}\nfunc (fi bindataFileInfo) ModTime() time.Time {\n\treturn fi.modTime\n}\nfunc (fi bindataFileInfo) IsDir() bool {\n\treturn false\n}\nfunc (fi bindataFileInfo) Sys() interface{} {\n\treturn nil\n}\n\nvar _migrationMysql100_initial_schemaSql = []byte(\"\\x1f\\x8b\\x08\\x00\\x00\\x09\\x6e\\x88\\x00\\xff\\xac\\x54\\xd1\\x6e\\xda\\x30\\x14\\x7d\\xcf\\x57\\xdc\\xb7\\x82\\x96\\x4c\\x50\\x8d\\x6d\\x12\\xda\\x43\\x20\\x66\\x8b\\x16\\x02\\x0b\\xce\\xd4\\x3e\\x45\\xae\\x73\\x01\\x8b\\xe0\\x58\\xb1\\x51\\xbb\\x7d\\xfd\\xe4\\xa4\\xa1\\x29\\x6c\\x15\\x5a\\xcb\\x13\\xbe\\x3e\\xf7\\x9c\\x63\\xfb\\xe4\\x7a\\x1e\\xbc\\xdb\\x8b\\x4d\\xc5\\x0c\\x42\\xaa\\x1c\\xcf\\x83\\xd5\\x8f\\x08\\x84\\x04\\x8d\\xdc\\x88\\x52\\xc2\\x55\\xaa\\xae\\x40\\x68\\xc0\\x07\\xe4\\x07\\x83\\x39\\xdc\\x6f\\x51\\x82\\xd9\\x0a\\x0d\\x4d\\x9f\\x05\\x09\\x0d\\x4c\\xa9\\x42\\x60\\xee\\x4c\\x13\\xe2\\x53\\x02\\xd4\\x9f\\x44\\x04\\xc2\\x19\\xc4\\x0b\\x0a\\xe4\\x26\\x5c\\xd1\\x15\\x68\\xbe\\xc5\\xfc\\x50\\xa0\\x86\\x9e\\x03\\x00\\x20\\x72\\x08\\x63\\x5a\\x23\\xe2\\x34\\x8a\\xc0\\x4f\\xe9\\x22\\x0b\\xe3\\x69\\x42\\xe6\\x24\\xa6\\xb0\\x4c\\xc2\\xb9\\x9f\\xdc\\xc2\\x77\\x72\\xeb\\xd6\\xf8\\xdc\\x9a\\x6c\\x7f\\x81\\x55\\x69\\x5b\\xdd\\xb6\\xea\\x79\\x0d\\xaa\\x5c\\x83\\xc1\\x6a\\x2f\\x64\\xe3\\xaf\\x55\\x76\\xed\\xc9\\x8a\\x92\\xb3\\x02\\x8c\\xd8\\x23\\xfc\\x2e\\x25\\xd6\\xd4\\xf5\\xaa\\x4b\\x4d\\xc3\\xf9\\x09\\xbd\\xe7\\x35\\x28\\x21\\x21\\xa5\\xd3\\xf7\\x30\\x41\\xce\\x0e\\xba\\x91\\xb2\\xf5\\x5c\\xac\\xd7\\x58\\xa1\\xe4\\xe8\\xc2\\x9e\\xfd\\x7a\\x5c\\xc3\\xba\\x2a\\xf7\\xb5\\xa7\\x5a\\x87\\x29\\x75\\x94\\x81\\x9f\\x7e\\x32\\xfd\\xe6\\x27\\xbd\\xd1\\xf0\\xba\\xff\\xa4\\xd5\\xe0\\x38\\x2f\\x0f\\xd2\\x3c\\xc7\\x0d\\x07\\x83\\x53\\x5c\\x85\\x1b\\x7b\\xbe\\x13\\xbe\\x41\\x1f\\x3a\\xde\\x3d\\x0f\\xac\\xcf\\xbb\\x82\\xc9\\x1d\\x68\\x53\\x09\\xb9\\x01\\x53\\x82\\x90\\xb9\\xe0\\xf6\\xae\\x64\\x69\\x40\\x55\\xa8\\x51\\x9a\\x9a\\x53\\x1b\\xc6\\x77\\xa7\\x1e\\xaf\\x47\\xa3\\xfe\\x2b\\x38\\x79\\x71\\xd0\\x06\\xab\\xe7\\x9c\\x9f\\x3e\\x7e\\x7e\\x0d\\x67\\x18\\x07\\xe4\\xa6\\xbe\\xda\\x4c\\xc8\\x1c\\x1f\\xa0\\x67\\xff\\xf7\\xeb\\xbd\\xbe\\x43\\xe2\\xaf\\x61\\x4c\\xbe\\x84\\x52\\x96\\xc1\\x64\\xec\\xbc\\x94\\xcb\\x4e\\x52\\xfe\\x37\\x9a\\x6f\\xfd\\xae\\x17\\xbc\\xc1\\xa5\\xf7\\xfa\\x72\\x4e\\x4e\\xec\\xe9\\xcd\\xf9\\x31\\x86\\x83\\x73\\x7f\\x42\\x6a\\xc3\\x24\\xc7\\x4c\\xe4\\x4f\\xc0\\x0f\\x67\\xb2\\x3b\\x51\\x14\\x98\\x67\\xcc\\xfc\\xf5\\xb3\\x82\\x80\\xcc\\xfc\\x34\\xa2\\x30\\x4d\\x93\\x84\\xc4\\x34\\xb3\\xbb\\x2b\\xea\\xcf\\x97\\x4d\\x77\\x81\\x4c\\x6f\\x31\\x6f\\xdc\\x4c\\x16\\x8b\\x88\\xf8\\xf1\\x79\\xf3\\xcc\\x8f\\x56\\xc4\\xed\\x24\\x82\\x29\\x95\\x1d\\x85\\xdb\\x68\\x30\\xa5\\xdc\\x63\\xf1\\x5f\\x19\\x71\\xba\\xd3\\x30\\x28\\xef\\x65\\x3b\\x0f\\x8f\\xc3\\xd0\\x16\\x2f\\x1a\\x87\\x55\\x69\\xb5\\xe0\\x8e\\xf1\\x9d\\x13\\x24\\x8b\\xe5\\x63\\xf0\\x8e\\x23\\x70\\xdc\\xad\\x76\\x03\\x38\\x76\\xfe\\x04\\x00\\x00\\xff\\xff\\x9f\\x18\\xee\\xd3\\x93\\x05\\x00\\x00\")\n\nfunc migrationMysql100_initial_schemaSqlBytes() ([]byte, error) {\n\treturn bindataRead(\n\t\t_migrationMysql100_initial_schemaSql,\n\t\t\"migration/mysql/1.0.0_initial_schema.sql\",\n\t)\n}\n\nfunc migrationMysql100_initial_schemaSql() (*asset, error) {\n\tbytes, err := migrationMysql100_initial_schemaSqlBytes()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinfo := bindataFileInfo{name: \"migration/mysql/1.0.0_initial_schema.sql\", size: 1427, mode: os.FileMode(420), modTime: time.Unix(1477837063, 0)}\n\ta := &asset{bytes: bytes, info: info}\n\treturn a, nil\n}\n\n// Asset loads and returns the asset for the given name.\n// It returns an error if the asset could not be found or\n// could not be loaded.\nfunc Asset(name string) ([]byte, error) {\n\tcannonicalName := strings.Replace(name, \"\\\\\", \"/\", -1)\n\tif f, ok := _bindata[cannonicalName]; ok {\n\t\ta, err := f()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Asset %s can't read by error: %v\", name, err)\n\t\t}\n\t\treturn a.bytes, nil\n\t}\n\treturn nil, fmt.Errorf(\"Asset %s not found\", name)\n}\n\n// MustAsset is like Asset but panics when Asset would return an error.\n// It simplifies safe initialization of global variables.\nfunc MustAsset(name string) []byte {\n\ta, err := Asset(name)\n\tif err != nil {\n\t\tpanic(\"asset: Asset(\" + name + \"): \" + err.Error())\n\t}\n\n\treturn a\n}\n\n// AssetInfo loads and returns the asset info for the given name.\n// It returns an error if the asset could not be found or\n// could not be loaded.\nfunc AssetInfo(name string) (os.FileInfo, error) {\n\tcannonicalName := strings.Replace(name, \"\\\\\", \"/\", -1)\n\tif f, ok := _bindata[cannonicalName]; ok {\n\t\ta, err := f()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"AssetInfo %s can't read by error: %v\", name, err)\n\t\t}\n\t\treturn a.info, nil\n\t}\n\treturn nil, fmt.Errorf(\"AssetInfo %s not found\", name)\n}\n\n// AssetNames returns the names of the assets.\nfunc AssetNames() []string {\n\tnames := make([]string, 0, len(_bindata))\n\tfor name := range _bindata {\n\t\tnames = append(names, name)\n\t}\n\treturn names\n}\n\n// _bindata is a table, holding each asset generator, mapped to its name.\nvar _bindata = map[string]func() (*asset, error){\n\t\"migration/mysql/1.0.0_initial_schema.sql\": migrationMysql100_initial_schemaSql,\n}\n\n// AssetDir returns the file names below a certain\n// directory embedded in the file by go-bindata.\n// For example if you run go-bindata on data/... and data contains the\n// following hierarchy:\n//     data/\n//       foo.txt\n//       img/\n//         a.png\n//         b.png\n// then AssetDir(\"data\") would return []string{\"foo.txt\", \"img\"}\n// AssetDir(\"data/img\") would return []string{\"a.png\", \"b.png\"}\n// AssetDir(\"foo.txt\") and AssetDir(\"notexist\") would return an error\n// AssetDir(\"\") will return []string{\"data\"}.\nfunc AssetDir(name string) ([]string, error) {\n\tnode := _bintree\n\tif len(name) != 0 {\n\t\tcannonicalName := strings.Replace(name, \"\\\\\", \"/\", -1)\n\t\tpathList := strings.Split(cannonicalName, \"/\")\n\t\tfor _, p := range pathList {\n\t\t\tnode = node.Children[p]\n\t\t\tif node == nil {\n\t\t\t\treturn nil, fmt.Errorf(\"Asset %s not found\", name)\n\t\t\t}\n\t\t}\n\t}\n\tif node.Func != nil {\n\t\treturn nil, fmt.Errorf(\"Asset %s not found\", name)\n\t}\n\trv := make([]string, 0, len(node.Children))\n\tfor childName := range node.Children {\n\t\trv = append(rv, childName)\n\t}\n\treturn rv, nil\n}\n\ntype bintree struct {\n\tFunc     func() (*asset, error)\n\tChildren map[string]*bintree\n}\n\nvar _bintree = &bintree{nil, map[string]*bintree{\n\t\"migration\": {nil, map[string]*bintree{\n\t\t\"mysql\": {nil, map[string]*bintree{\n\t\t\t\"1.0.0_initial_schema.sql\": {migrationMysql100_initial_schemaSql, map[string]*bintree{}},\n\t\t}},\n\t}},\n}}\n\n// RestoreAsset restores an asset under the given directory\nfunc RestoreAsset(dir, name string) error {\n\tdata, err := Asset(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tinfo, err := AssetInfo(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// RestoreAssets restores an asset under the given directory recursively\nfunc RestoreAssets(dir, name string) error {\n\tchildren, err := AssetDir(name)\n\t// File\n\tif err != nil {\n\t\treturn RestoreAsset(dir, name)\n\t}\n\t// Dir\n\tfor _, child := range children {\n\t\terr = RestoreAssets(dir, filepath.Join(name, child))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc _filePath(dir, name string) string {\n\tcannonicalName := strings.Replace(name, \"\\\\\", \"/\", -1)\n\treturn filepath.Join(append([]string{dir}, strings.Split(cannonicalName, \"/\")...)...)\n}\n"
  },
  {
    "path": "migration/mysql/1.0.0_initial_schema.sql",
    "content": "-- +migrate Up\n-- SQL in section 'Up' is executed when this migration is applied\nCREATE TABLE IF NOT EXISTS schedules (\n    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n    date         DATE NOT NULL,        -- date of termination schedule, in local time zone\n    time         DATETIME NOT NULL,    -- time in UTC. Because of time difference, may differ from date\n    app          VARCHAR(512) NOT NULL,\n    account      VARCHAR(100) NOT NULL,\n    region       VARCHAR(50)  NOT NULL, -- use blank string to indicate not present\n    stack        VARCHAR(255) NOT NULL, -- use blank string to indicate not present\n    cluster      VARCHAR(768) NOT NULL, -- use blank string to indicate not present\n    INDEX date_index (date)\n    )\nENGINE=InnoDB;\n\nCREATE TABLE IF NOT EXISTS terminations (\n    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n    app          VARCHAR(512) NOT NULL,\n    account      VARCHAR(100) NOT NULL,\n    stack        VARCHAR(255) NOT NULL,\n    cluster      VARCHAR(768) NOT NULL,\n    region       VARCHAR(50) NOT NULL,\n    asg          VARCHAR(1000) NOT NULL,\n    instance_id  VARCHAR(48) NOT NULL,\n    killed_at    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    leashed      BOOLEAN NOT NULL DEFAULT FALSE,\n    INDEX app_killed_at_index (app,killed_at)\n    )\nENGINE=InnoDB;\n\n\n-- +migrate Down\n-- SQL section 'Down' is executed when this migration is rolled back\nDROP TABLE schedules;\nDROP TABLE terminations;\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: Chaos Monkey\nsite_url: https://netflix.github.io/chaosmonkey\nrepo_url: https://github.com/Netflix/chaosmonkey\ntheme: readthedocs\ncopyright: <b>A Netflix Original Production</b><br><a href=\"http://netflix.github.io/\">Netflix OSS</a> | <a href=\"http://techblog.netflix.com/\">Tech Blog</a> | <a href=\"https://twitter.com/NetflixOSS\">Twitter @NetflixOSS</a> | <a href=\"https://jobs.netflix.com\">Jobs</a>\npages:\n  - Home: index.md\n  - How to deploy: How-to-deploy.md\n  - Configuration file format: Configuration-file-format.md\n  - Configuring behavior via Spinnaker: Configuring-behavior-via-spinnaker.md\n  - Termination behaior: Termination-behavior.md\n  - Running locally: Running-locally.md\n  - Plugins:\n      - Home: plugins/index.md\n      - Decryptor: plugins/Decryptor.md\n      - Error counter: plugins/Error-counter.md\n      - Outage checker: plugins/Outage-checker.md\n      - Tracker: plugins/Tracker.md\n      - Constrainer: plugins/Constrainer.md\n  - Development:\n      - Running tests: dev/Running-tests.md\n      - Vendoring dependencies: dev/Vendoring-dependencies.md\n\n"
  },
  {
    "path": "mock/configgetter.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mock\n\nimport \"github.com/Netflix/chaosmonkey/v2\"\n\n// ConfigGetter implements chaosmonkey.Getter\ntype ConfigGetter struct {\n\tConfig chaosmonkey.AppConfig\n}\n\n// NewConfigGetter returns a mock config getter that always returns the specified config\nfunc NewConfigGetter(config chaosmonkey.AppConfig) ConfigGetter {\n\treturn ConfigGetter{Config: config}\n}\n\n// DefaultConfigGetter returns a mock config getter that always returns the same config\nfunc DefaultConfigGetter() ConfigGetter {\n\treturn ConfigGetter{\n\t\tConfig: chaosmonkey.AppConfig{\n\t\t\tEnabled:                        true,\n\t\t\tRegionsAreIndependent:          true,\n\t\t\tMeanTimeBetweenKillsInWorkDays: 5,\n\t\t\tMinTimeBetweenKillsInWorkDays:  1,\n\t\t\tGrouping:                       chaosmonkey.Cluster,\n\t\t\tExceptions:                     nil,\n\t\t},\n\t}\n}\n\n// Get implements chaosmonkey.Getter.Get\nfunc (c ConfigGetter) Get(app string) (*chaosmonkey.AppConfig, error) {\n\treturn &c.Config, nil\n}\n"
  },
  {
    "path": "mock/deployment.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mock\n\nimport (\n\t\"github.com/pkg/errors\"\n\n\tD \"github.com/Netflix/chaosmonkey/v2/deploy\"\n)\n\nconst cloudProvider = \"aws\"\n\n// Dep returns a mock implementation of deploy.Deployment\n// Dep has 4 apps: foo, bar, baz, quux\n// Each app runs in 1 account:\n//\n//\tfoo, bar, baz run in prod\n//\tquux runs in test\n//\n// Each app has one cluster: foo-prod, bar-prod, baz-prod\n// Each cluster runs in one region: us-east-1\n// Each cluster contains 1 AZ with two instances\nfunc Dep() D.Deployment {\n\tprod := D.AccountName(\"prod\")\n\ttest := D.AccountName(\"test\")\n\tusEast1 := D.RegionName(\"us-east-1\")\n\n\treturn &Deployment{map[string]D.AppMap{\n\t\t\"foo\":  {prod: D.AccountInfo{CloudProvider: cloudProvider, Clusters: D.ClusterMap{\"foo-prod\": {usEast1: {\"foo-prod-v001\": []D.InstanceID{\"i-d3e3d611\", \"i-63f52e25\"}}}}}},\n\t\t\"bar\":  {prod: D.AccountInfo{CloudProvider: cloudProvider, Clusters: D.ClusterMap{\"bar-prod\": {usEast1: {\"bar-prod-v011\": []D.InstanceID{\"i-d7f06d45\", \"i-ce433cf1\"}}}}}},\n\t\t\"baz\":  {prod: D.AccountInfo{CloudProvider: cloudProvider, Clusters: D.ClusterMap{\"baz-prod\": {usEast1: {\"baz-prod-v004\": []D.InstanceID{\"i-25b86646\", \"i-573d46d5\"}}}}}},\n\t\t\"quux\": {test: D.AccountInfo{CloudProvider: cloudProvider, Clusters: D.ClusterMap{\"quux-test\": {usEast1: {\"quux-test-v004\": []D.InstanceID{\"i-25b866ab\", \"i-892d46d5\"}}}}}},\n\t}}\n}\n\n// NewDeployment returns a mock implementation of deploy.Deployment\n// Pass in a deploy.AppMap, for example:\n//\n//\t map[string]deploy.AppMap{\n//\t\t\t\"foo\":  deploy.AppMap{\"prod\": {\"foo-prod\": {\"us-east-1\": {\"foo-prod-v001\": []string{\"i-d3e3d611\", \"i-63f52e25\"}}}}},\n//\t\t\t\"bar\":  deploy.AppMap{\"prod\": {\"bar-prod\": {\"us-east-1\": {\"bar-prod-v011\": []string{\"i-d7f06d45\", \"i-ce433cf1\"}}}}},\n//\t\t\t\"baz\":  deploy.AppMap{\"prod\": {\"baz-prod\": {\"us-east-1\": {\"baz-prod-v004\": []string{\"i-25b86646\", \"i-573d46d5\"}}}}},\n//\t\t\t\"quux\": deploy.AppMap{\"test\": {\"quux-test\": {\"us-east-1\": {\"quux-test-v004\": []string{\"i-25b866ab\", \"i-892d46d5\"}}}}},\n//\t\t}\nfunc NewDeployment(apps map[string]D.AppMap) D.Deployment {\n\treturn &Deployment{apps}\n}\n\n// Deployment implements deploy.Deployment interface\ntype Deployment struct {\n\tAppMap map[string]D.AppMap\n}\n\n// Apps implements deploy.Deployment.Apps\nfunc (d Deployment) Apps(c chan<- *D.App, apps []string) {\n\tdefer close(c)\n\n\tfor name, appmap := range d.AppMap {\n\t\tc <- D.NewApp(name, appmap)\n\t}\n}\n\n// GetClusterNames implements deploy.Deployment.GetClusterNames\nfunc (d Deployment) GetClusterNames(app string, account D.AccountName) ([]D.ClusterName, error) {\n\tresult := make([]D.ClusterName, 0)\n\tfor cluster := range d.AppMap[app][account].Clusters {\n\t\tresult = append(result, cluster)\n\t}\n\n\treturn result, nil\n}\n\n// GetRegionNames implements deploy.Deployment.GetRegionNames\nfunc (d Deployment) GetRegionNames(app string, account D.AccountName, cluster D.ClusterName) ([]D.RegionName, error) {\n\tresult := make([]D.RegionName, 0)\n\tfor region := range d.AppMap[app][account].Clusters[cluster] {\n\t\tresult = append(result, region)\n\t}\n\n\treturn result, nil\n}\n\n// AppNames implements deploy.Deployment.AppNames\nfunc (d Deployment) AppNames() ([]string, error) {\n\tresult := make([]string, len(d.AppMap), len(d.AppMap))\n\ti := 0\n\tfor app := range d.AppMap {\n\t\tresult[i] = app\n\t\ti++\n\t}\n\n\treturn result, nil\n}\n\n// GetApp implements deploy.Deployment.GetApp\nfunc (d Deployment) GetApp(name string) (*D.App, error) {\n\treturn D.NewApp(name, d.AppMap[name]), nil\n}\n\n// CloudProvider implements deploy.Deployment.CloudProvider\nfunc (d Deployment) CloudProvider(account string) (string, error) {\n\treturn cloudProvider, nil\n}\n\n// GetInstanceIDs implements deploy.Deployment.GetInstanceIDs\nfunc (d Deployment) GetInstanceIDs(app string, account D.AccountName, cloudProvider string, region D.RegionName, cluster D.ClusterName) (D.ASGName, []D.InstanceID, error) {\n\t// Return an error if the cluster doesn't exist in the region\n\n\tappInfo, ok := d.AppMap[app]\n\tif !ok {\n\t\treturn \"\", nil, errors.Errorf(\"no app %s\", app)\n\t}\n\n\taccountInfo, ok := appInfo[account]\n\tif !ok {\n\t\treturn \"\", nil, errors.Errorf(\"app %s not deployed in account %s\", app, account)\n\t}\n\n\tclusterInfo, ok := accountInfo.Clusters[cluster]\n\tif !ok {\n\t\treturn \"\", nil, errors.Errorf(\"no cluster %s in app:%s, account:%s\", cluster, app, account)\n\t}\n\n\tasgs, ok := clusterInfo[region]\n\tif !ok {\n\t\treturn \"\", nil, errors.Errorf(\"cluster %s in account %s not deployed in region %s\", cluster, account, region)\n\t}\n\n\tinstances := make([]D.InstanceID, 0)\n\n\t// We assume there's only one asg, and retrieve the instances\n\tvar asg D.ASGName\n\tvar ids []D.InstanceID\n\n\tfor asg, ids = range asgs {\n\t\tfor _, id := range ids {\n\t\t\tinstances = append(instances, id)\n\t\t}\n\t}\n\n\treturn asg, instances, nil\n}\n"
  },
  {
    "path": "mock/deps.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mock\n\nimport (\n\t\"io/ioutil\"\n\t\"time\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/clock\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/config/param\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n)\n\ntype (\n\t// Checker implements deps.Checker\n\tChecker struct {\n\t\tError error\n\t}\n\n\t// Tracker implements chaosmonkey.Tracker\n\tTracker struct {\n\t\tError error\n\t}\n\n\t// ErrorCounter implements chaosmonkey.Publisher\n\tErrorCounter struct{}\n\n\t// Clock implements clock.Clock\n\tClock struct {\n\t\tTime time.Time\n\t}\n\n\t// Env implements chaosmonkey.Env\n\tEnv struct {\n\t\tIsInTest bool\n\t}\n)\n\n// Check implements deps.Checker.Check\nfunc (c Checker) Check(term chaosmonkey.Termination, appCfg chaosmonkey.AppConfig, endHour int, loc *time.Location) error {\n\treturn c.Error\n\n}\n\n// Track implements chaosmonkey.Tracker.Track\nfunc (t Tracker) Track(trm chaosmonkey.Termination) error {\n\treturn t.Error\n}\n\n// Increment implements chaosmonkey.ErrorCounter.Increment\nfunc (e ErrorCounter) Increment() error {\n\treturn nil\n}\n\n// Now implements clock.Clock.Now\nfunc (c Clock) Now() time.Time {\n\treturn c.Time\n}\n\n// InTest implements chaosmonkey.Env.InTest\nfunc (e Env) InTest() bool {\n\treturn e.IsInTest\n}\n\n// Deps returns a deps.Deps object that contains mocks.\n// The mocks implement their interfaces by performing no-ops.\nfunc Deps() deps.Deps {\n\tcfg := config.Defaults()\n\tcfg.Set(param.Enabled, true)\n\tcfg.Set(param.Leashed, false)\n\tcfg.Set(param.Accounts, []string{\"prod\", \"test\"})\n\n\tf, err := ioutil.TempFile(\"\", \"cm-test\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// The ioutil.TempFile opens the file, but we\n\t// don't need it open, since we are just going\n\t// to pass the file name along via the CronPath\n\t// function, so we just close it\n\terr = f.Close()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcfg.Set(param.CronPath, f.Name())\n\n\treturn deps.Deps{\n\t\tMonkeyCfg:  cfg,\n\t\tChecker:    Checker{Error: nil},\n\t\tConfGetter: DefaultConfigGetter(),\n\t\tCl:         clock.New(),\n\t\tDep:        Dep(),\n\t\tT:          new(Terminator),\n\t\tOu:         Outage{},\n\t\tErrCounter: ErrorCounter{},\n\t\tEnv:        Env{false},\n\t}\n}\n"
  },
  {
    "path": "mock/install.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mock\n\n// Executable is a mock representation of Chaosmonkey executable\ntype Executable struct {\n\t// Path returns the path to executable\n\tPath string\n}\n\n// ExecutablePath returns a mock implementation of command.CurrentExecutable.ExecutablePath\nfunc (m Executable) ExecutablePath() (string, error) {\n\treturn m.Path, nil\n}\n"
  },
  {
    "path": "mock/instance.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mock\n\n// Instance implements instance.Instance\ntype Instance struct {\n\tApp, Account, Stack, Cluster, Region, ASG, InstanceID string\n}\n\n// AppName implements instance.AppName\nfunc (i Instance) AppName() string {\n\treturn i.App\n}\n\n// AccountName implements instance.AccountName\nfunc (i Instance) AccountName() string {\n\treturn i.Account\n}\n\n// RegionName implements instance.RegionName\nfunc (i Instance) RegionName() string {\n\treturn i.Region\n}\n\n// StackName implements instance.StackName\nfunc (i Instance) StackName() string {\n\treturn i.Stack\n}\n\n// ClusterName implements instance.ClusterName\nfunc (i Instance) ClusterName() string {\n\treturn i.Cluster\n}\n\n// ASGName implements instance.ASGName\nfunc (i Instance) ASGName() string {\n\treturn i.ASG\n}\n\n// ID implements instance.ID\nfunc (i Instance) ID() string {\n\treturn i.InstanceID\n}\n\n// CloudProvider implements instance.IsContainer\nfunc (i Instance) CloudProvider() string {\n\treturn \"aws\"\n}\n"
  },
  {
    "path": "mock/mock.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package mock contains helper functions for generating mock objects\n// for testing\npackage mock\n\nimport D \"github.com/Netflix/chaosmonkey/v2/deploy\"\n\n// AppFactory creates App objects used for testing\ntype AppFactory struct {\n}\n\n// App creates a mock App\nfunc (factory AppFactory) App() *D.App {\n\n\tvar m = D.AppMap{\n\t\t\"prod\": D.AccountInfo{\n\t\t\tCloudProvider: \"aws\",\n\t\t\tClusters: D.ClusterMap{\n\t\t\t\t\"abc-prod\": {\n\t\t\t\t\t\"us-east-1\": {\n\t\t\t\t\t\t\"abc-prod-v017\": []D.InstanceID{\"i-f60b22e8\", \"i-1b17963b\", \"i-7c0c8af4\"},\n\t\t\t\t\t},\n\t\t\t\t\t\"us-west-2\": {\n\t\t\t\t\t\t\"abc-prod-v017\": []D.InstanceID{\"i-8b42d04e\", \"i-52ead2f0\", \"i-b6261b80\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"test\": D.AccountInfo{\n\t\t\tCloudProvider: \"aws\",\n\t\t\tClusters: D.ClusterMap{\n\t\t\t\t\"abc-beta\": {\n\t\t\t\t\t\"us-east-1\": {\n\t\t\t\t\t\t\"abc-beta-v031\": []D.InstanceID{\"i-c8a5458c\", \"i-61f55db3\", \"i-6a820363\"},\n\t\t\t\t\t\t\"abc-beta-v030\": []D.InstanceID{\"i-c41206b7\", \"i-c8a5458c\", \"i-6a820363\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\treturn D.NewApp(\"abc\", m)\n}\n"
  },
  {
    "path": "mock/outage.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mock\n\n// Outage is a mock implementation of outage.Outage\ntype Outage struct{}\n\n// Outage implemnets outage.Outage.Outage\nfunc (o Outage) Outage() (bool, error) {\n\treturn false, nil\n}\n"
  },
  {
    "path": "mock/terminator.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mock\n\nimport \"github.com/Netflix/chaosmonkey/v2\"\n\n// Terminator implements term.terminator\ntype Terminator struct {\n\tInstance chaosmonkey.Instance\n\tNcalls   int\n\tError    error\n}\n\n// Execute pretends to terminate an instance\nfunc (t *Terminator) Execute(trm chaosmonkey.Termination) error {\n\t// Records the most recent killed instance for assertion checking\n\tt.Instance = trm.Instance\n\n\t// Records how many times it's been invoked\n\tt.Ncalls++\n\n\treturn t.Error\n}\n"
  },
  {
    "path": "mysql/checker_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n//go:build docker\n// +build docker\n\n// The tests in this package use docker to test against a mysql:8.0 database\n// By default, the tests are off unless you pass the \"-tags docker\" flag\n// when running the test.\n\npackage mysql_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\tc \"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/mock\"\n\t\"github.com/Netflix/chaosmonkey/v2/mysql\"\n)\n\nvar endHour = 15 // 3PM\n\n// testSetup returns some values useful for test setup\nfunc testSetup(t *testing.T) (ins c.Instance, loc *time.Location, appCfg c.AppConfig) {\n\n\tins = mock.Instance{\n\t\tApp:        \"myapp\",\n\t\tAccount:    \"prod\",\n\t\tStack:      \"mystack\",\n\t\tCluster:    \"mycluster\",\n\t\tRegion:     \"us-east-1\",\n\t\tASG:        \"myapp-mystack-mycluster-V123\",\n\t\tInstanceID: \"i-a96a0166\",\n\t}\n\n\tloc, err := time.LoadLocation(\"America/Los_Angeles\")\n\tif err != nil {\n\t\tt.Fatalf(err.Error())\n\t}\n\n\tappCfg = c.AppConfig{\n\t\tEnabled:                        true,\n\t\tRegionsAreIndependent:          true,\n\t\tMeanTimeBetweenKillsInWorkDays: 5,\n\t\tMinTimeBetweenKillsInWorkDays:  1,\n\t\tGrouping:                       c.Cluster,\n\t\tExceptions:                     nil,\n\t}\n\n\treturn\n\n}\n\n// TestCheckPermitted verifies check succeeds when no previous terminations in database\nfunc TestCheckPermitted(t *testing.T) {\n\terr := initDB()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tm, err := mysql.New(\"localhost\", port, \"root\", password, \"chaosmonkey\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tins, loc, appCfg := testSetup(t)\n\n\ttrm := c.Termination{Instance: ins, Time: time.Now(), Leashed: false}\n\n\terr = m.Check(trm, appCfg, endHour, loc)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// TestCheckPermitted verifies check fails if commit is too recent\nfunc TestCheckForbidden(t *testing.T) {\n\terr := initDB()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tm, err := mysql.New(\"localhost\", port, \"root\", password, \"chaosmonkey\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tins, loc, appCfg := testSetup(t)\n\n\ttrm := c.Termination{Instance: ins, Time: time.Now(), Leashed: false}\n\n\t// First check should succeed\n\terr = m.Check(trm, appCfg, endHour, loc)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Second check should fail\n\terr = m.Check(trm, appCfg, endHour, loc)\n\tif err == nil {\n\t\tt.Fatal(\"Check() succeeded when it should have failed\")\n\t}\n\n\tif _, ok := err.(c.ErrViolatesMinTime); !ok {\n\t\tt.Fatalf(\"Expected Err.ViolatesMinTime, got %v\", err)\n\t}\n}\n\n// When we are going to commit an unleashed termination, we only care\n// about unleashed previous terminations\nfunc TestCheckLeashed(t *testing.T) {\n\terr := initDB()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tm, err := mysql.New(\"localhost\", port, \"root\", password, \"chaosmonkey\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tins, loc, appCfg := testSetup(t)\n\n\ttrm := c.Termination{Instance: ins, Time: time.Now(), Leashed: true}\n\n\t// First check should succeed\n\terr = m.Check(trm, appCfg, endHour, loc)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttrm = c.Termination{Instance: ins, Time: time.Now(), Leashed: false}\n\n\t// Second check should fail\n\terr = m.Check(trm, appCfg, endHour, loc)\n\n\tif err != nil {\n\t\tt.Fatalf(\"Should have allowed an unleashed termination after leashed: %v\", err)\n\t}\n}\n\n// Check that only termination is permitted on concurrent attempts\nfunc TestConcurrentChecks(t *testing.T) {\n\terr := initDB()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tm, err := mysql.New(\"localhost\", port, \"root\", password, \"chaosmonkey\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tins, loc, appCfg := testSetup(t)\n\n\ttrm := c.Termination{Instance: ins, Time: time.Now()}\n\n\t// Try to check twice. At least one should return an error\n\tch := make(chan error, 2)\n\n\tgo func() {\n\t\t// We use the \"MySQL.CheckWithDelay\" method which adds a delay between reading\n\t\t// from the database and writing to it, to increase the likelihood that\n\t\t// the two requests overlap\n\t\tch <- m.CheckWithDelay(trm, appCfg, endHour, loc, 1*time.Second)\n\t}()\n\n\tgo func() {\n\t\tch <- m.Check(trm, appCfg, endHour, loc)\n\t}()\n\n\tvar success int\n\tvar txDeadlock int\n\tvar violatesMinTime int\n\tfor i := 0; i < 2; i++ {\n\t\terr := <-ch\n\t\tswitch {\n\t\tcase err == nil:\n\t\t\tsuccess++\n\t\tcase mysql.TxDeadlock(err):\n\t\t\ttxDeadlock++\n\t\tcase mysql.ViolatesMinTime(err):\n\t\t\tviolatesMinTime++\n\t\tdefault:\n\t\t\tt.Fatalf(\"Unexpected error: %+v\", err)\n\t\t}\n\t}\n\n\tif got, want := success, 1; got != want {\n\t\tt.Errorf(\"got %d succeses, want: %d\", got, want)\n\t}\n}\n\nfunc TestCombinations(t *testing.T) {\n\n\t// Reference instance\n\tins := mock.Instance{\n\t\tApp:        \"myapp\",\n\t\tAccount:    \"prod\",\n\t\tStack:      \"mystack\",\n\t\tCluster:    \"mycluster\",\n\t\tRegion:     \"us-east-1\",\n\t\tASG:        \"myapp-mystack-mycluster-V123\",\n\t\tInstanceID: \"i-a96a0166\",\n\t}\n\n\tloc, err := time.LoadLocation(\"America/Los_Angeles\")\n\tif err != nil {\n\t\tt.Fatalf(err.Error())\n\t}\n\n\ttests := []struct {\n\t\tdesc    string\n\t\tgrp     c.Group\n\t\treg     bool // regions are independent\n\t\tins     c.Instance\n\t\tallowed bool // true if we can kill this instance after previous\n\t}{\n\t\t{\"same cluster, should fail\", c.Cluster, true, mock.Instance{App: \"myapp\", Account: \"prod\", Stack: \"mystack\", Cluster: \"mycluster\", Region: \"us-east-1\", ASG: \"myapp-mystack-mycluster-V123\"}, false},\n\n\t\t{\"different cluster, should succeed\", c.Cluster, true, mock.Instance{App: \"myapp\", Account: \"prod\", Stack: \"mystack\", Cluster: \"othercluster\", Region: \"us-east-1\", ASG: \"myapp-mystack-mycluster-V123\"}, true},\n\n\t\t{\"same stack should fail\", c.Stack, true, mock.Instance{App: \"myapp\", Account: \"prod\", Stack: \"mystack\", Cluster: \"othercluster\", Region: \"us-east-1\", ASG: \"myapp-mystack-mycluster-V123\"}, false},\n\n\t\t{\"different stack, should succeed\", c.Stack, true, mock.Instance{App: \"myapp\", Account: \"prod\", Stack: \"otherstack\", Cluster: \"othercluster\", Region: \"us-east-1\", ASG: \"myapp-otherstack-mycluster-V123\"}, true},\n\n\t\t{\"same app, should fail\", c.App, true, mock.Instance{App: \"myapp\", Account: \"prod\", Stack: \"mystack\", Cluster: \"othercluster\", Region: \"us-east-1\", ASG: \"myapp-mystack-mycluster-V123\"}, false},\n\n\t\t{\"different region, should succeed\", c.Cluster, true, mock.Instance{App: \"myapp\", Account: \"prod\", Stack: \"mystack\", Cluster: \"mycluster\", Region: \"us-west-2\", ASG: \"myapp-mystack-mycluster-V123\"}, true},\n\n\t\t{\"different region where regions are not independent, should fail\", c.Cluster, false, mock.Instance{App: \"myapp\", Account: \"prod\", Stack: \"mystack\", Cluster: \"mycluster\", Region: \"us-west-2\", ASG: \"myapp-mystack-mycluster-V123\"}, false},\n\t}\n\n\tfor _, tt := range tests {\n\n\t\terr := initDB()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tm, err := mysql.New(\"localhost\", port, \"root\", password, \"chaosmonkey\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tcfg := c.AppConfig{\n\t\t\tEnabled:                        true,\n\t\t\tRegionsAreIndependent:          tt.reg,\n\t\t\tMeanTimeBetweenKillsInWorkDays: 1,\n\t\t\tMinTimeBetweenKillsInWorkDays:  1,\n\t\t\tGrouping:                       tt.grp,\n\t\t}\n\n\t\terr = m.Check(c.Termination{Instance: ins, Time: time.Now()}, cfg, endHour, loc)\n\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tterm := c.Termination{Instance: tt.ins, Time: time.Now()}\n\n\t\terr = m.Check(term, cfg, endHour, loc)\n\t\tif tt.allowed && err != nil {\n\t\t\tt.Errorf(\"%s: got m.Check(%#v, %#v) = %+v, expected nil\", tt.desc, term, cfg, err)\n\t\t}\n\n\t\tif !tt.allowed && err == nil {\n\t\t\tt.Errorf(\"%s: get m.Check(%#v, %#v) = nil, expected error\", tt.desc, term, cfg)\n\t\t}\n\n\t}\n}\n\nfunc TestCheckMinTimeEnforced(t *testing.T) {\n\n\tcfg := c.AppConfig{\n\t\tEnabled:                        true,\n\t\tRegionsAreIndependent:          true,\n\t\tMeanTimeBetweenKillsInWorkDays: 5,\n\t\tMinTimeBetweenKillsInWorkDays:  2,\n\t\tGrouping:                       c.Cluster,\n\t}\n\n\t// The current kill time\n\tnow := \"Thu Dec 17 11:35:00 2015 -0800\"\n\n\t// Since MinTimeBetweenKillsInWorkDays is 1 here, then the most recent\n\t// kill permitted is the day before at endHour\n\tendHour := 15\n\n\t// Tue Dec 15 15:00:00 2015 -0800\n\n\t// Any kills later than that time will not be permitted\n\t// Boundary value testing!\n\n\t// this is a magic date used by go for parsing strings\n\trefDate := \"Mon Jan  2 15:04:05 2006 -0700\"\n\ttnow, err := time.Parse(refDate, now)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tins := mock.Instance{\n\t\tApp:        \"myapp\",\n\t\tAccount:    \"prod\",\n\t\tStack:      \"mystack\",\n\t\tCluster:    \"mycluster\",\n\t\tRegion:     \"us-east-1\",\n\t\tASG:        \"myapp-mystack-mycluster-V123\",\n\t\tInstanceID: \"i-a96a0166\",\n\t}\n\n\tloc, err := time.LoadLocation(\"America/Los_Angeles\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttests := []struct {\n\t\tlast    string\n\t\tallowed bool\n\t}{\n\t\t{\"Tue Dec 15 15:01:00 2015 -0800\", false},\n\t\t{\"Tue Dec 15 14:59:59 2015 -0800\", true},\n\t}\n\n\tfor _, tt := range tests {\n\n\t\terr := initDB()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tm, err := mysql.New(\"localhost\", port, \"root\", password, \"chaosmonkey\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t//\n\t\t// Write the initial termination\n\t\t//\n\n\t\tlast, err := time.Parse(\"Mon Jan  2 15:04:05 2006 -0700\", tt.last)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\terr = m.Check(c.Termination{Instance: ins, Time: last}, cfg, endHour, loc)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to write the initial termination, should always succeed: %v\", err)\n\t\t}\n\n\t\t//\n\t\t// Write today's termination\n\t\t//\n\n\t\terr = m.Check(c.Termination{Instance: ins, Time: tnow}, cfg, endHour, loc)\n\n\t\tswitch err.(type) {\n\t\tcase nil:\n\t\t\tif !tt.allowed {\n\t\t\t\tt.Fatalf(\"%s termination should have been forbidden, was allowed\", tt.last)\n\t\t\t}\n\t\tcase c.ErrViolatesMinTime:\n\t\t\tif tt.allowed {\n\t\t\t\tt.Errorf(\"%s termination should have been allowed, got: %v\", tt.last, err)\n\t\t\t}\n\t\tdefault:\n\t\t\tt.Errorf(\"%s termination returned unexpected err: %v\", tt.last, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "mysql/mysql.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mysql\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/cal\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/config/param\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n\t\"github.com/Netflix/chaosmonkey/v2/migration\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedstore\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedule\"\n\t\"github.com/rubenv/sql-migrate\"\n\t\"log\"\n)\n\n// MySQL represents a MySQL-backed store for schedules and terminations\ntype MySQL struct {\n\tdb *sql.DB\n}\n\n// TxDeadlock returns true if the error is because of a transaction deadlock\nfunc TxDeadlock(err error) bool {\n\tswitch err := errors.Cause(err).(type) {\n\tcase *mysql.MySQLError:\n\t\t// ER_LOCK_DEADLOCK\n\t\t// See: https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_lock_deadlock\n\t\treturn err.Number == 1213\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// ViolatesMinTime returns true if the error violates min time between\n// terminations\nfunc ViolatesMinTime(err error) bool {\n\t_, ok := errors.Cause(err).(chaosmonkey.ErrViolatesMinTime)\n\treturn ok\n}\n\n// NewFromConfig creates a new MySQL taking config parameters from cfg\nfunc NewFromConfig(cfg *config.Monkey) (MySQL, error) {\n\n\tif cfg.DatabaseHost() == \"\" {\n\t\treturn MySQL{}, errors.Errorf(\"%s not specified\", param.DatabaseHost)\n\t}\n\n\tencryptedPassword := cfg.DatabaseEncryptedPassword()\n\n\tdecryptor, err := deps.GetDecryptor(cfg)\n\tif err != nil {\n\t\treturn MySQL{}, err\n\t}\n\n\tpassword, err := decryptor.Decrypt(encryptedPassword)\n\tif err != nil {\n\t\treturn MySQL{}, err\n\t}\n\n\treturn New(cfg.DatabaseHost(), cfg.DatabasePort(), cfg.DatabaseUser(), password, cfg.DatabaseName())\n}\n\n// New creates a new MySQL\nfunc New(host string, port int, user string, password string, dbname string) (MySQL, error) {\n\tdb, err := sql.Open(\"mysql\", dsn(host, port, user, password, dbname))\n\tif err != nil {\n\t\treturn MySQL{}, errors.Wrap(err, \"sql.Open failed\")\n\t}\n\n\treturn MySQL{db}, nil\n}\n\n// Close closes the underlying sql.DB\nfunc (m MySQL) Close() error {\n\treturn m.db.Close()\n}\n\n// utcDate takes a time.Time in a local time zone and returns a time.Time\n// that has the same year/month/day as date, but is in UTC, at 12 PM\n// We use this to work with MySQL DATE entries without having to worry about\n// MySQL changing the value due to time conversion\nfunc utcDate(date time.Time) time.Time {\n\tyear, month, day := date.Date()\n\treturn time.Date(year, month, day, 12, 0, 0, 0, time.UTC)\n}\n\n// Retrieve  retrieves the schedule for the given date\nfunc (m MySQL) Retrieve(date time.Time) (sched *schedule.Schedule, err error) {\n\trows, err := m.db.Query(\"SELECT time, app, account, region, stack, cluster FROM schedules WHERE date = DATE(?)\", utcDate(date))\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed to retrieve schedule for %s\", date)\n\t}\n\n\tsched = schedule.New()\n\n\tdefer func() {\n\t\tif cerr := rows.Close(); cerr != nil && err == nil {\n\t\t\terr = errors.Wrap(cerr, \"rows.Close() failed\")\n\t\t}\n\t}()\n\n\tfor rows.Next() {\n\t\tvar tm time.Time\n\t\tvar app, account, region, stack, cluster string\n\n\t\terr = rows.Scan(&tm, &app, &account, &region, &stack, &cluster)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to scan row\")\n\t\t}\n\n\t\tsched.Add(tm, grp.New(app, account, region, stack, cluster))\n\t}\n\n\terr = rows.Err()\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"rows.Err() errored\")\n\t}\n\n\treturn sched, nil\n\n}\n\n// Publish publishes the schedule for the given date\nfunc (m MySQL) Publish(date time.Time, sched *schedule.Schedule) error {\n\treturn m.PublishWithDelay(date, sched, 0)\n}\n\n// PublishWithDelay publishes the schedule with a delay between checking the schedule\n// exists and writing it. The delay is used only for testing race conditions\nfunc (m MySQL) PublishWithDelay(date time.Time, sched *schedule.Schedule, delay time.Duration) (err error) {\n\t// First, we check to see if there is a schedule present\n\ttx, err := m.db.Begin()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to begin transaction\")\n\t}\n\n\t// We must either commit or rollback at the end\n\tdefer func() {\n\t\tswitch err {\n\t\tcase nil:\n\t\t\terr = tx.Commit()\n\t\tcase schedstore.ErrAlreadyExists:\n\t\t\t// We want to return ErrAlreadyExists even if the transaction commit\n\t\t\t// fails\n\t\t\t_ = tx.Commit()\n\t\tdefault:\n\t\t\t_ = tx.Rollback()\n\t\t}\n\t}()\n\n\texists, err := schedExists(tx, date)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif exists {\n\t\treturn schedstore.ErrAlreadyExists\n\t}\n\n\tif delay > 0 {\n\t\ttime.Sleep(delay)\n\t}\n\tquery := \"INSERT INTO schedules (date, time, app, account, region, stack, cluster) VALUES (?, ?, ?, ?, ?, ?, ?)\"\n\tstmt, err := tx.Prepare(query)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to prepare sql statement: %s\", query)\n\t}\n\n\tfor _, entry := range sched.Entries() {\n\t\tvar app, account, region, stack, cluster string\n\t\tapp = entry.Group.App()\n\t\taccount = entry.Group.Account()\n\t\tif val, ok := entry.Group.Region(); ok {\n\t\t\tregion = val\n\t\t}\n\t\tif val, ok := entry.Group.Stack(); ok {\n\t\t\tstack = val\n\t\t}\n\t\tif val, ok := entry.Group.Cluster(); ok {\n\t\t\tcluster = val\n\t\t}\n\n\t\t_, err = stmt.Exec(utcDate(date), entry.Time.In(time.UTC), app, account, region, stack, cluster)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"failed to execute prepared query\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// schedExists returns true if a schedule has previously been\n// published for this date\nfunc schedExists(tx *sql.Tx, date time.Time) (result bool, err error) {\n\trows, err := tx.Query(\"SELECT COUNT(*) FROM schedules WHERE date = DATE(?)\", date)\n\tif err != nil {\n\t\treturn false, errors.Wrapf(err, \"failed to check if schedule exists for %s\", date)\n\t}\n\n\tvar count int\n\n\tdefer func() {\n\t\tif cerr := rows.Close(); cerr != nil && err == nil {\n\t\t\terr = errors.Wrap(err, \"rows.Close() failed\")\n\t\t}\n\t}()\n\n\tfor rows.Next() {\n\t\terr = rows.Scan(&count)\n\t\tif err != nil {\n\t\t\treturn false, errors.Wrap(err, \"failed to scan row\")\n\t\t}\n\t}\n\n\treturn (count > 0), nil\n\n}\n\n// dsn returns a MySQL TCP connection string (data source name)\n// See: https://github.com/go-sql-driver/mysql#dsn-data-source-name\nfunc dsn(host string, port int, user string, password string, dbname string) string {\n\tparams := map[string]string{\n\t\t\"transaction_isolation\": \"SERIALIZABLE\", // we need serializable transactions for atomic test & set behavior\n\t\t\"parseTime\":             \"true\",         // enable us to use sql.Rows.Scan to read time.Time objects from queries\n\t\t\"loc\":                   \"UTC\",          // Scan'd time.Times should be treated as being in UTC time zone\n\t\t\"time_zone\":             \"UTC\",          // MySQL should interpret DATETIME values as being in UTC\n\t}\n\n\tvar ss []string\n\n\tfor k, v := range params {\n\t\tss = append(ss, fmt.Sprintf(\"%s=%s\", k, v))\n\t}\n\n\tquery := strings.Join(ss, \"&\")\n\n\treturn fmt.Sprintf(\"%s:%s@tcp(%s:%d)/%s?%s\", user, password, host, port, dbname, query)\n}\n\n// Check checks if a termination is permitted and, if so, records the\n// termination time on the server\nfunc (m MySQL) Check(term chaosmonkey.Termination, appCfg chaosmonkey.AppConfig, endHour int, loc *time.Location) error {\n\treturn m.CheckWithDelay(term, appCfg, endHour, loc, 0)\n}\n\n// CheckWithDelay is the same as Check, but adds a delay between reading and\n// writing to the database (used for testing only)\nfunc (m MySQL) CheckWithDelay(term chaosmonkey.Termination, appCfg chaosmonkey.AppConfig, endHour int, loc *time.Location, delay time.Duration) error {\n\ttx, err := m.db.Begin()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to begin transaction\")\n\t}\n\n\tdefer func() {\n\t\tswitch err {\n\t\tcase nil:\n\t\t\terr = tx.Commit()\n\t\tdefault:\n\t\t\t_ = tx.Rollback()\n\t\t}\n\t}()\n\n\terr = respectsMinTimeBetweenKills(tx, term.Time, term, appCfg, endHour, loc)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif delay > 0 {\n\t\ttime.Sleep(delay)\n\t}\n\n\terr = recordTermination(tx, term, loc)\n\treturn err\n\n}\n\n// respectsMinTimeBetweenKills checks if this termination will respect or\n// violate the min time between kills value. If this termination is too close\n// to the most recent one, this will return an error.\n// If this termination would violate the min time, returns an ErrViolatesMinTime\nfunc respectsMinTimeBetweenKills(tx *sql.Tx, now time.Time, term chaosmonkey.Termination, appCfg chaosmonkey.AppConfig, endHour int, loc *time.Location) (err error) {\n\tapp := term.Instance.AppName()\n\taccount := term.Instance.AccountName()\n\tthreshold, err := noKillsSince(appCfg.MinTimeBetweenKillsInWorkDays, now, endHour, loc)\n\tif err != nil {\n\t\treturn err\n\t}\n\tquery := \"SELECT instance_id, killed_at FROM terminations WHERE app = ? AND account = ? AND killed_at >= ?\"\n\n\tvar rows *sql.Rows\n\n\targs := []interface{}{app, account, threshold.In(time.UTC)}\n\n\tswitch appCfg.Grouping {\n\tcase chaosmonkey.App:\n\t\t// nothing to do\n\tcase chaosmonkey.Stack:\n\t\tquery += \" AND stack = ?\"\n\t\targs = append(args, term.Instance.StackName())\n\tcase chaosmonkey.Cluster:\n\t\tquery += \" AND cluster = ?\"\n\t\targs = append(args, term.Instance.ClusterName())\n\tdefault:\n\t\treturn errors.Errorf(\"unknown group: %v\", appCfg.Grouping)\n\t}\n\n\tif appCfg.RegionsAreIndependent {\n\t\tquery += \" AND region = ?\"\n\t\targs = append(args, term.Instance.RegionName())\n\t}\n\n\t// For unleashed (real) terminations, we only care about previous\n\t// terminations that were also unleashed. That's because a previous\n\t// leashed termination wasn't a real one, so that wouldn't violate\n\t// the min time between terminations\n\tif !term.Leashed {\n\t\tquery += \" AND leashed = FALSE\"\n\t}\n\n\t// We need at most one entry\n\tquery += \" LIMIT 1\"\n\n\trows, err = tx.Query(query, args...)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\tcerr := rows.Close()\n\t\tif err == nil && cerr != nil {\n\t\t\terr = cerr\n\t\t}\n\t}()\n\n\tif rows.Next() {\n\t\tvar instanceID string\n\t\tvar killedAt time.Time\n\t\terr = rows.Scan(&instanceID, &killedAt)\n\t\treturn chaosmonkey.ErrViolatesMinTime{InstanceID: instanceID, KilledAt: killedAt, Loc: loc}\n\t}\n\n\treturn nil\n}\n\n// noKillsSince computes the date of the most recent kill\n// that conforms to the min time between kills specified\n// by days\n//\n// Note that the calculation is min time in work days, so it does not count weekends.\n//\n// chrono is an interface for returning the current time\n// endHour is the hour of the end of a workday in 24-hour time. For example, if\n// workday ends at 5PM, this would be 17\n// loc is the location that corresponds to endHour, e.g. America/Los_Angeles for PST\n//\n// # The returned time will be in UTC\n//\n// If days=1, then we allow\n// kills each day, so the most recent kill will be at the\n// end of the previous workday. For example:\n//\n//\tdays: 1\n//\tendHour: 17 (i.e. work day ends at 5PM local time)\n//\tloc:  America/Los_Angeles (PST)\n//\tchrono.Now(): Wed, Dec. 16, 2015 2:30 PM PST\n//\tOutput: Tue, Dec. 15, 2015 5:00 PM PST\n//\n// If days=0, returns the current date, with\n// the time set to endHour. For example:\n//\n//\tdays: 0\n//\tendHour: 17 (i.e. work day ends at 5PM local time)\n//\tloc:  America/Los_Angeles (PST)\n//\tchrono.Now(): Wed, Dec. 16, 2015 2:30 PM PST\n//\tOutput: Wed, Dec. 16, 2015 5:00 PM PST\n//\n// noKillsSince returns the a datetime that is the last allowed time that a kill\n// is permitted to have happened.\nfunc noKillsSince(days int, now time.Time, endHour int, loc *time.Location) (time.Time, error) {\n\tif days < 0 {\n\t\treturn time.Time{}, errors.Errorf(\"noKillsSince passed illegal input: days=%d\", days)\n\t}\n\n\toneDay := time.Hour * 24\n\n\t// Tail-recursive helper function reads clearer than writing a\n\t// traditional loop\n\t//\n\t// It expects a time localized to the zone associated with endHour because\n\t// workday and year-month-day values depend on the local timezone\n\tvar helper func(N int, tInLoc time.Time) time.Time\n\n\thelper = func(N int, tInLoc time.Time) time.Time {\n\t\tswitch {\n\t\tcase !cal.IsWorkday(tInLoc):\n\t\t\treturn helper(N, tInLoc.Add(-oneDay))\n\t\tcase N == 0:\n\t\t\treturn time.Date(tInLoc.Year(), tInLoc.Month(), tInLoc.Day(), endHour, 0, 0, 0, loc).UTC()\n\t\tdefault:\n\t\t\treturn helper(N-1, tInLoc.Add(-oneDay))\n\t\t}\n\t}\n\n\treturn helper(days, now.In(loc)), nil\n}\n\nfunc recordTermination(tx *sql.Tx, term chaosmonkey.Termination, loc *time.Location) (err error) {\n\n\ti := term.Instance\n\n\t_, err = tx.Exec(\"INSERT INTO terminations (app, account, stack, cluster, region, asg, instance_id, killed_at, leashed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\",\n\t\ti.AppName(), i.AccountName(), i.StackName(), i.ClusterName(), i.RegionName(), i.ASGName(), i.ID(), term.Time.In(time.UTC), term.Leashed)\n\n\treturn err\n}\n\nvar migrationSource = &migrate.AssetMigrationSource{\n\tAsset:    migration.Asset,\n\tAssetDir: migration.AssetDir,\n\tDir:      \"migration/mysql\",\n}\n\nvar databaseDialect = \"mysql\"\n\n// Migrate upgrades a database to the latest database schema version.\nfunc Migrate(mysqlDb MySQL) error {\n\tmigrationCount, err := migrate.Exec(mysqlDb.db, databaseDialect, migrationSource, migrate.Up)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"database migration failed\")\n\t}\n\tlog.Println(\"Successfully applied database migrations. Number of migrations applied: \", migrationCount)\n\n\treturn nil\n}\n"
  },
  {
    "path": "mysql/mysql_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n//go:build docker\n// +build docker\n\n// The tests in this package use docker to test against a mysql:8.0 database\n// By default, the tests are off unless you pass the \"-tags docker\" flag\n// when running the test.\n//\n// By default, TestMain starts up a new mysql Docker container. However, if you\n// already have a mysql docker container running, you can skip this by also\n// passing the \"dockerup\" flag: -tags \"docker dockerup\"\n\npackage mysql_test\n\nimport (\n\t\"bufio\"\n\t\"database/sql\"\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/mysql\"\n\t\"github.com/pkg/errors\"\n)\n\nvar (\n\tdbName   string = \"chaosmonkey\"\n\tpassword string = \"password\"\n\tport     int    = 3306\n)\n\n// inUse returns true if port accepts connections on localhsot\nfunc inUse(port int) bool {\n\tconn, err := net.Dial(\"tcp\", fmt.Sprintf(\"127.0.0.1:%d\", port))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tconn.Close()\n\treturn true\n}\n\nfunc TestMain(m *testing.M) {\n\n\t//\n\t// Setup\n\t//\n\n\tvar alwaysUp bool\n\tflag.BoolVar(&alwaysUp, \"dockerup\", false, \"if true, won't start docker\")\n\tflag.Parse()\n\n\tvar cmd *exec.Cmd\n\tvar err error\n\n\tif !alwaysUp {\n\t\t// Check to make sure the port isn't already in use\n\t\tif inUse(port) {\n\t\t\tpanic(fmt.Sprintf(\"can't start mysql container: port %d currently in use\", port))\n\t\t}\n\t\tcmd, err = startMySQLContainer()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t//\n\t// Run tests\n\t//\n\n\tr := m.Run()\n\n\t//\n\t// Cleanup\n\t//\n\n\tif !alwaysUp {\n\t\t// Send a SIGTERM once we're done so mysql container shuts down\n\t\tcmd.Process.Signal(syscall.SIGTERM)\n\n\t\t// Wait for container to finish shutting down\n\t\tcmd.Wait()\n\t}\n\n\tos.Exit(r)\n}\n\n// startMySQLContainer starts a MySQL docker container\n// Returns the Cmd object associated with the process\nfunc startMySQLContainer() (*exec.Cmd, error) {\n\tcmd := exec.Command(\"docker\", \"run\", \"-e\", \"MYSQL_ROOT_PASSWORD=\"+password, fmt.Sprintf(\"-p3306:%d\", port), \"mysql:8.0\")\n\tpipe, err := cmd.StderrPipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tch := make(chan int)\n\n\treadyString := \"mysqld: ready for connections\"\n\n\tgo func() {\n\t\treader := bufio.NewReader(pipe)\n\n\t\t// We loop until we see mysqld: ready for connections\n\t\tvar s string\n\n\t\tfor !strings.Contains(s, readyString) {\n\t\t\ts, err = reader.ReadString('\\n')\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfmt.Print(s)\n\t\t}\n\n\t\tch <- 0\n\n\t}()\n\n\tselect {\n\tcase <-ch:\n\t\t// noting to do\n\tcase <-time.After(time.Second * 30):\n\t\t// timeout.\n\t\treturn nil, errors.Errorf(`never saw \"%s\". (mysql container needs manual cleanup)`, readyString)\n\t}\n\n\tfmt.Println(\"Sleeping for 5 seconds\")\n\ttime.Sleep(5 * time.Second)\n\n\treturn cmd, nil\n}\n\n// initDB initializes the \"chaosmonkey\" database with the chaosmonkey schemas\n// It wipes out any existing database database with the same name\nfunc initDB() error {\n\tdb, err := sql.Open(\"mysql\", fmt.Sprintf(\"root:%s@tcp(127.0.0.1:%d)/\", password, port))\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"sql.Open failed\")\n\t}\n\tdefer db.Close()\n\n\t_, err = db.Exec(\"DROP DATABASE IF EXISTS \" + dbName)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"drop database failed\")\n\t}\n\n\t_, err = db.Exec(\"CREATE DATABASE \" + dbName)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"create database failed\")\n\t}\n\n\tmysqlDb, dbErr := mysql.New(\"127.0.0.1\", port, \"root\", password, dbName)\n\tif dbErr != nil {\n\t\treturn errors.Wrap(err, \"mysql.New failed\")\n\t}\n\tdefer mysqlDb.Close()\n\n\t// Get the \"terminations\" schema\n\n\terr = mysql.Migrate(mysqlDb)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"database migration failed\")\n\t}\n\n\treturn nil\n\n}\n\nfunc stopMySQLContainer(name string, t *testing.T) {\n\n\t// Dump the output just in case\n\tcmd := exec.Command(\"docker\", \"logs\", name)\n\tdata, _ := cmd.CombinedOutput()\n\tt.Log(string(data))\n\n\tcmd = exec.Command(\"docker\", \"kill\", name)\n\tdata, err := cmd.CombinedOutput()\n\ts := string(data)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"docker kill errored (%v) with output: %s\", err, s))\n\t}\n\n\tcmd = exec.Command(\"docker\", \"rm\", name)\n\tdata, err = cmd.CombinedOutput()\n\ts = string(data)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"docker kill errored (%v) with output: %s\", err, s))\n\t}\n}\n"
  },
  {
    "path": "mysql/no_kills_since_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mysql\n\n// This file contains test for the config.NoKillsSince method\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\n/*\nTest scenarios to hit\n\n- Zero days between kills\n- one day\n- N days\n- Mid week\n- Beginning of week\n- Beginning of day\n- End of day\n- Daylight savings\n- All day boundaries\n\n*/\n\nfunc TestZeroDaysBetweenKills(t *testing.T) {\n\n\t// Note: -0800 = PST\n\t//       -0700 = PDT\n\ttests := []struct {\n\t\tdays  int\n\t\tnow   string\n\t\tsince string\n\t}{\n\t\t// 0 days means that kills are allowed on the same day\n\t\t{0, \"Thu Dec 17 00:00:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 00:00:01 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 00:01:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 08:59:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 08:59:59 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 09:00:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 09:01:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 14:18:30 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\n\t\t// Test on the UTC boundary (midnight UTC = 4PM)\n\t\t{0, \"Thu Dec 17 15:59:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 15:59:59 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 16:00:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 16:00:01 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 16:01:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 16:59:59 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 16:59:59 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 15:00:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 17:00:01 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\n\t\t// Test on the midnight boundary\n\t\t{0, \"Thu Dec 17 23:00:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 23:59:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Thu Dec 17 23:59:59 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{0, \"Fri Dec 18 00:00:00 2015 -0800\", \"Fri Dec 18 15:00:00 2015 -0800\"},\n\n\t\t// Go back 1 day\n\t\t{1, \"Thu Dec 17 00:00:00 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 00:00:01 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 00:01:00 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 08:59:00 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 08:59:59 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 09:00:00 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 09:01:00 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 15:00:00 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 15:00:01 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 15:18:30 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 15:59:00 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 15:59:59 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 16:00:00 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 16:00:01 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 16:01:00 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 16:59:59 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 16:59:59 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 23:00:00 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 23:59:00 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Thu Dec 17 23:59:59 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{1, \"Fri Dec 18 00:00:00 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\t\t{1, \"Fri Dec 18 00:00:01 2015 -0800\", \"Thu Dec 17 15:00:00 2015 -0800\"},\n\n\t\t// going back several days\n\t\t{1, \"Thu Dec 17 15:18:30 2015 -0800\", \"Wed Dec 16 15:00:00 2015 -0800\"},\n\t\t{2, \"Thu Dec 17 15:18:30 2015 -0800\", \"Tue Dec 15 15:00:00 2015 -0800\"},\n\t\t{3, \"Thu Dec 17 15:18:30 2015 -0800\", \"Mon Dec 14 15:00:00 2015 -0800\"},\n\t\t{4, \"Thu Dec 17 15:18:30 2015 -0800\", \"Fri Dec 11 15:00:00 2015 -0800\"},\n\t\t{5, \"Thu Dec 17 15:18:30 2015 -0800\", \"Thu Dec 10 15:00:00 2015 -0800\"},\n\t\t{6, \"Thu Dec 17 15:18:30 2015 -0800\", \"Wed Dec  9 15:00:00 2015 -0800\"},\n\t\t{7, \"Thu Dec 17 15:18:30 2015 -0800\", \"Tue Dec  8 15:00:00 2015 -0800\"},\n\t\t{8, \"Thu Dec 17 15:18:30 2015 -0800\", \"Mon Dec  7 15:00:00 2015 -0800\"},\n\t\t{9, \"Thu Dec 17 15:18:30 2015 -0800\", \"Fri Dec  4 15:00:00 2015 -0800\"},\n\n\t\t// beginning of week\n\t\t{2, \"Mon Dec 14 00:00:00 2015 -0800\", \"Thu Dec 10 15:00:00 2015 -0800\"},\n\t\t{2, \"Mon Dec 14 08:59:59 2015 -0800\", \"Thu Dec 10 15:00:00 2015 -0800\"},\n\t\t{2, \"Mon Dec 14 09:00:00 2015 -0800\", \"Thu Dec 10 15:00:00 2015 -0800\"},\n\t\t{2, \"Mon Dec 14 09:01:00 2015 -0800\", \"Thu Dec 10 15:00:00 2015 -0800\"},\n\t\t{2, \"Mon Dec 14 15:00:00 2015 -0800\", \"Thu Dec 10 15:00:00 2015 -0800\"},\n\t\t{2, \"Mon Dec 14 15:01:00 2015 -0800\", \"Thu Dec 10 15:00:00 2015 -0800\"},\n\t\t{2, \"Mon Dec 14 23:59:59 2015 -0800\", \"Thu Dec 10 15:00:00 2015 -0800\"},\n\n\t\t// daylight savings in 2016:\n\t\t// Sun Mar 13 02:00:00 2015 -0700\n\t\t// Sun Nov  6 02:00:00 2015 -0800\n\n\t\t// test inside of DST and on the boundaries\n\t\t{1, \"Mon Mar 14 12:35:46 2016 -0700\", \"Fri Mar 11 15:00:00 2016 -0800\"},\n\t\t{2, \"Tue Apr 12 12:35:46 2016 -0700\", \"Fri Apr  8 15:00:00 2016 -0700\"},\n\t\t{2, \"Tue Nov  8 12:35:46 2016 -0800\", \"Fri Nov  4 15:00:00 2016 -0700\"},\n\n\t\t// year boundary. Note: this'll break when we support holidays as\n\t\t// non-workdays\n\t\t{1, \"Fri Jan  1 12:05:00 2016 -0800\", \"Thu Dec 31 15:00:00 2015 -0800\"},\n\n\t\t// try a larger number\n\t\t{30, \"Fri Dec 18 11:45:11 2015 -0800\", \"Fri Nov  6 15:00:00 2015 -0800\"},\n\t}\n\n\ttz, err := time.LoadLocation(\"America/Los_Angeles\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tendHour := 15 // typically we run chaos monkey until 3PM\n\tfor _, tt := range tests {\n\t\tgot, err := noKillsSince(tt.days, parse(tt.now), endHour, tz)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif want := parse(tt.since); got != want {\n\t\t\tt.Errorf(\"noKillsSince(%d, \\\"%s\\\")=\\\"%s\\\", want \\\"%s\\\"\", tt.days, tt.now, format(got.In(tz)), format(want.In(tz)))\n\t\t}\n\t}\n}\n\n// parse returns a time formatted as the standard output of \"date\", e.g.:\n// Thu Dec 17 15:18:30 PST 2015\nfunc parse(s string) time.Time {\n\tt, err := time.Parse(\"Mon Jan  2 15:04:05 2006 -0700\", s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn t.UTC()\n}\n\n// format returns a formatted string representing a time, to simplify debugging\nfunc format(tm time.Time) string {\n\treturn tm.Format(\"Mon Jan  2 15:04:05 2006 -0700\")\n}\n"
  },
  {
    "path": "mysql/schedstore_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n//go:build docker\n// +build docker\n\n// The tests in this package use docker to test against a mysql:8.0 database\n// By default, the tests are off unless you pass the \"-tags docker\" flag\n// when running the test.\n\npackage mysql_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t_ \"github.com/go-sql-driver/mysql\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n\t\"github.com/Netflix/chaosmonkey/v2/mysql\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedstore\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedule\"\n)\n\n// Test we can publish and then retrieve a schedule\nfunc TestPublishRetrieve(t *testing.T) {\n\terr := initDB()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tm, err := mysql.New(\"localhost\", port, \"root\", password, \"chaosmonkey\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tloc, err := time.LoadLocation(\"America/Los_Angeles\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tsched := schedule.New()\n\n\tt1 := time.Date(2016, time.June, 20, 11, 40, 0, 0, loc)\n\tsched.Add(t1, grp.New(\"chaosguineapig\", \"test\", \"us-east-1\", \"\", \"chaosguineapig-test\"))\n\n\tdate := time.Date(2016, time.June, 20, 0, 0, 0, 0, loc)\n\n\t// Code under test:\n\terr = m.Publish(date, sched)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsched, err = m.Retrieve(date)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tentries := sched.Entries()\n\tif got, want := len(entries), 1; got != want {\n\t\tt.Fatalf(\"got len(entries)=%d, want %d\", got, want)\n\t}\n\n\tentry := entries[0]\n\n\tif !t1.Equal(entry.Time) {\n\t\tt.Errorf(\"%s != %s\", t1, entry.Time)\n\t}\n}\n\nfunc NewMySQL() (mysql.MySQL, error) {\n\treturn mysql.New(\"localhost\", port, \"root\", password, \"chaosmonkey\")\n}\n\nfunc TestPublishRetrieveMultipleEntries(t *testing.T) {\n\terr := initDB()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tm, err := NewMySQL()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tloc, err := time.LoadLocation(\"America/Los_Angeles\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tpsched := schedule.New()\n\n\tpEntries := []schedule.Entry{\n\t\t{Time: time.Date(2016, time.June, 20, 11, 40, 0, 0, loc), Group: grp.New(\"doesnotexist\", \"test\", \"us-east-1\", \"\", \"doesnotexist-foo-bar\")},\n\t\t{Time: time.Date(2016, time.June, 20, 12, 35, 0, 0, loc), Group: grp.New(\"foobar\", \"other\", \"us-west-2\", \"\", \"foobar-baz-quux\")},\n\t\t{Time: time.Date(2016, time.June, 20, 9, 7, 0, 0, loc), Group: grp.New(\"chaosguineapig\", \"prod\", \"us-east-1\", \"\", \"chaosguineapig-prod\")},\n\t}\n\n\tfor _, v := range pEntries {\n\t\tpsched.Add(v.Time, v.Group)\n\t}\n\n\tdate := time.Date(2016, time.June, 20, 0, 0, 0, 0, loc)\n\n\t// Code under test:\n\terr = m.Publish(date, psched)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\trsched, err := m.Retrieve(date)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\trEntries := rsched.Entries()\n\tif got, want := len(rEntries), len(pEntries); got != want {\n\t\tt.Fatalf(\"got len(entries)=%d, want %d\", got, want)\n\t}\n\n\tfor i := range pEntries {\n\t\tif got, want := rEntries[i], pEntries[i]; !got.Equal(&want) {\n\t\t\tt.Errorf(\"got entry[%d]=%v, want %v\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestScheduleAlreadyExists(t *testing.T) {\n\terr := initDB()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tm, err := NewMySQL()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tloc, err := time.LoadLocation(\"America/Los_Angeles\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tpsched1 := schedule.New()\n\tpsched1.Add(time.Date(2016, time.June, 20, 11, 40, 0, 0, loc), grp.New(\"imaginaryproject\", \"test\", \"us-west-2\", \"\", \"\"))\n\n\tdate := time.Date(2016, time.June, 20, 0, 0, 0, 0, loc)\n\n\t// Create an initial schedule with one entry\n\terr = m.Publish(date, psched1)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Try to publish a new schedule\n\n\tpEntries := []schedule.Entry{\n\t\t{Time: time.Date(2016, time.June, 20, 11, 40, 0, 0, loc), Group: grp.New(\"doesnotexist\", \"test\", \"us-east-1\", \"\", \"doesnotexist-foo-bar\")},\n\t\t{Time: time.Date(2016, time.June, 20, 12, 35, 0, 0, loc), Group: grp.New(\"foobar\", \"other\", \"us-west-2\", \"\", \"foobar-baz-quux\")},\n\t\t{Time: time.Date(2016, time.June, 20, 9, 7, 0, 0, loc), Group: grp.New(\"chaosguineapig\", \"prod\", \"us-east-1\", \"\", \"chaosguineapig-prod\")},\n\t}\n\n\tpsched2 := schedule.New()\n\tfor _, v := range pEntries {\n\t\tpsched2.Add(v.Time, v.Group)\n\t}\n\n\terr = m.Publish(date, psched2)\n\n\t// This should return an error\n\tif got, want := err, schedstore.ErrAlreadyExists; got != want {\n\t\tt.Fatalf(`got m.Publish()=\"%v\" want \"%v\"`, got, want)\n\t}\n}\n\nfunc TestScheduleAlreadyExistsConcurrency(t *testing.T) {\n\terr := initDB()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tm, err := NewMySQL()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tloc, err := time.LoadLocation(\"America/Los_Angeles\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tpsched1 := schedule.New()\n\tpsched1.Add(time.Date(2016, time.June, 20, 11, 40, 0, 0, loc), grp.New(\"imaginaryproject\", \"test\", \"us-west-2\", \"\", \"\"))\n\n\tpEntries := []schedule.Entry{\n\t\t{Time: time.Date(2016, time.June, 20, 11, 40, 0, 0, loc), Group: grp.New(\"doesnotexist\", \"test\", \"us-east-1\", \"\", \"doesnotexist-foo-bar\")},\n\t\t{Time: time.Date(2016, time.June, 20, 12, 35, 0, 0, loc), Group: grp.New(\"foobar\", \"other\", \"us-west-2\", \"\", \"foobar-baz-quux\")},\n\t\t{Time: time.Date(2016, time.June, 20, 9, 7, 0, 0, loc), Group: grp.New(\"chaosguineapig\", \"prod\", \"us-east-1\", \"\", \"chaosguineapig-prod\")},\n\t}\n\n\tpsched2 := schedule.New()\n\tfor _, v := range pEntries {\n\t\tpsched2.Add(v.Time, v.Group)\n\t}\n\n\t// Try to publish the schedule twice. At least one schedule should return an\n\t// error\n\tch := make(chan error, 2)\n\n\tdate := time.Date(2016, time.June, 20, 0, 0, 0, 0, loc)\n\n\tgo func() {\n\t\tch <- m.PublishWithDelay(date, psched1, 3*time.Second)\n\t}()\n\n\tgo func() {\n\t\tch <- m.PublishWithDelay(date, psched2, 0)\n\t}()\n\n\t// Retrieve the two error values from the two calls\n\n\tvar success int\n\tvar txDeadlock int\n\tfor i := 0; i < 2; i++ {\n\t\terr := <-ch\n\t\tswitch {\n\t\tcase err == nil:\n\t\t\tsuccess++\n\t\tcase mysql.TxDeadlock(err):\n\t\t\ttxDeadlock++\n\t\tdefault:\n\t\t\tt.Fatalf(\"Unexpected error: %+v\", err)\n\t\t}\n\t}\n\n\tif got, want := success, 1; got != want {\n\t\tt.Errorf(\"got %d succeses, want: %d\", got, want)\n\t}\n\n\t// Should cause a deadlock\n\tif got, want := txDeadlock, 1; got != want {\n\t\tt.Errorf(\"got %d txDeadlock, want: %d\", got, want)\n\t}\n}\n\nfunc TestOnlyReturnsFromDayRequested(t *testing.T) {\n\terr := initDB()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tm, err := NewMySQL()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tloc, err := time.LoadLocation(\"America/Los_Angeles\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Day 1: 6/20/2016: 1 entry\n\tpsched1 := schedule.New()\n\td1 := time.Date(2016, time.June, 20, 0, 0, 0, 0, loc)\n\tpsched1.Add(time.Date(2016, time.June, 20, 11, 40, 0, 0, loc), grp.New(\"imaginaryproject\", \"test\", \"us-west-2\", \"\", \"\"))\n\n\terr = m.Publish(d1, psched1)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Day 2: 6/21/2016: 3 entries\n\tpsched2 := schedule.New()\n\td2 := time.Date(2016, time.June, 21, 0, 0, 0, 0, loc)\n\tpEntries := []schedule.Entry{\n\t\t{Time: time.Date(2016, time.June, 21, 11, 40, 0, 0, loc), Group: grp.New(\"doesnotexist\", \"test\", \"us-east-1\", \"\", \"doesnotexist-foo-bar\")},\n\t\t{Time: time.Date(2016, time.June, 21, 12, 35, 0, 0, loc), Group: grp.New(\"foobar\", \"other\", \"us-west-2\", \"\", \"foobar-baz-quux\")},\n\t\t{Time: time.Date(2016, time.June, 21, 9, 7, 0, 0, loc), Group: grp.New(\"chaosguineapig\", \"prod\", \"us-east-1\", \"\", \"chaosguineapig-prod\")},\n\t}\n\n\tfor _, v := range pEntries {\n\t\tpsched2.Add(v.Time, v.Group)\n\t}\n\n\tm.Publish(d2, psched2)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttests := []struct {\n\t\tdate    time.Time\n\t\tentries int\n\t}{\n\t\t{d1, 1},\n\t\t{d2, 3},\n\t}\n\n\tfor _, tt := range tests {\n\t\tsched, err := m.Retrieve(tt.date)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif got, want := len(sched.Entries()), tt.entries; got != want {\n\t\t\tt.Fatalf(\"got len(entries)=%d, want %d\", got, want)\n\t\t}\n\t}\n}\n\nfunc TestNoScheduleRetrievedOnWrongDay(t *testing.T) {\n\terr := initDB()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tm, err := NewMySQL()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tloc, err := time.LoadLocation(\"America/Los_Angeles\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Day 1: 6/20/2016: 1 entry\n\tpsched := schedule.New()\n\td := time.Date(2016, time.June, 20, 0, 0, 0, 0, loc)\n\tpsched.Add(time.Date(2016, time.June, 20, 11, 40, 0, 0, loc), grp.New(\"imaginaryproject\", \"test\", \"us-west-2\", \"\", \"\"))\n\n\tm.Publish(d, psched)\n\n\ttests := []struct {\n\t\tdate    time.Time\n\t\tentries int\n\t}{\n\t\t{time.Date(2016, time.June, 19, 0, 0, 0, 0, loc), 0},\n\t\t{time.Date(2016, time.June, 20, 0, 0, 0, 0, loc), 1},\n\t\t{time.Date(2016, time.June, 21, 0, 0, 0, 0, loc), 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tsched, err := m.Retrieve(tt.date)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif got, want := len(sched.Entries()), tt.entries; got != want {\n\t\t\tt.Fatalf(\"got len(entries)=%d, want %d\", got, want)\n\t\t}\n\t}\n}\n\nfunc TestPublishDateDifferentTimes(t *testing.T) {\n\tloc, err := time.LoadLocation(\"America/Los_Angeles\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttests := []struct {\n\t\tptime time.Time\n\t\trtime time.Time\n\t}{\n\t\t{time.Date(2016, time.June, 20, 0, 0, 0, 0, loc), time.Date(2016, time.June, 20, 0, 0, 0, 0, loc)},\n\t\t{time.Date(2016, time.June, 20, 12, 0, 0, 0, loc), time.Date(2016, time.June, 20, 12, 0, 0, 0, loc)},\n\t\t{time.Date(2016, time.June, 20, 0, 0, 0, 0, loc), time.Date(2016, time.June, 20, 16, 59, 59, 0, loc)},\n\t\t{time.Date(2016, time.June, 20, 0, 0, 0, 0, loc), time.Date(2016, time.June, 20, 17, 0, 0, 0, loc)}, // UTC boundary\n\t\t{time.Date(2016, time.June, 20, 0, 0, 0, 0, loc), time.Date(2016, time.June, 20, 17, 0, 1, 0, loc)},\n\t\t{time.Date(2016, time.June, 20, 0, 0, 0, 0, loc), time.Date(2016, time.June, 20, 23, 59, 59, 0, loc)},\n\t\t{time.Date(2016, time.June, 20, 23, 59, 59, 0, loc), time.Date(2016, time.June, 20, 23, 59, 59, 0, loc)},\n\t\t{time.Date(2016, time.June, 20, 0, 0, 0, 0, loc), time.Date(2016, time.June, 20, 23, 59, 59, 0, loc)},\n\t\t{time.Date(2016, time.June, 20, 23, 59, 59, 0, loc), time.Date(2016, time.June, 20, 0, 0, 0, 0, loc)},\n\t}\n\n\tfor _, tt := range tests {\n\t\terr := initDB()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tm, err := NewMySQL()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tpsched := schedule.New()\n\t\tpsched.Add(time.Date(2016, time.June, 20, 11, 40, 0, 0, loc), grp.New(\"imaginaryproject\", \"test\", \"us-west-2\", \"\", \"\"))\n\n\t\tm.Publish(tt.ptime, psched)\n\n\t\tsched, err := m.Retrieve(tt.rtime)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif got, want := len(sched.Entries()), 1; got != want {\n\t\t\tt.Fatalf(\"publish date:%v, retrieve date:%v, got len(entries)=%d, want %d\", tt.ptime, tt.rtime, got, want)\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "outage/outage.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package outage provides a default no-op outage implementation\npackage outage\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n\t\"github.com/pkg/errors\"\n)\n\n// NullOutage is a no-op outage checker\ntype NullOutage struct{}\n\n// Outage always returns false\nfunc (n NullOutage) Outage() (bool, error) {\n\treturn false, nil\n}\n\nfunc init() {\n\tdeps.GetOutage = GetOutage\n}\n\n// GetOutage returns a do-nothing outage checker\nfunc GetOutage(cfg *config.Monkey) (chaosmonkey.Outage, error) {\n\tchecker := cfg.OutageChecker()\n\tif checker != \"\" {\n\t\treturn nil, errors.Errorf(\"unknown outage provider: %s\", checker)\n\t}\n\treturn NullOutage{}, nil\n}\n"
  },
  {
    "path": "schedstore/schedstore.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage schedstore\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/schedule\"\n)\n\n// ErrAlreadyExists is returned when calling Publish if a schedule already\n// exists\nvar ErrAlreadyExists = errors.New(\"schedule already exists\")\n\n// SchedStore stores schedule of terminations\ntype SchedStore interface {\n\t// Retrieve retrieves the schedule for the given date\n\t// The date must be in the local time zone\n\tRetrieve(date time.Time) (*schedule.Schedule, error)\n\n\t// Publish publishes the schedule for the given date\n\t// The date must be in the local time zone\n\tPublish(date time.Time, sched *schedule.Schedule) error\n}\n"
  },
  {
    "path": "schedule/constrainer.go",
    "content": "// Copyright 2017 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage schedule\n\n// Constrainer provides additional constraints on a schedule\ntype Constrainer interface {\n\t// Produce a new schedule that satisfies constraints by eliminating scheduled terminations\n\tFilter(schedule Schedule) Schedule\n}\n"
  },
  {
    "path": "schedule/schedule.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package schedule implements a schedule of terminations\npackage schedule\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n)\n\n// Populate populates the termination schedule with the random\n// terminations for a list of apps. If the specified list of apps is empty,\n// then it will\nfunc (s *Schedule) Populate(d deploy.Deployment, getter chaosmonkey.AppConfigGetter, chaosConfig *config.Monkey, apps []string) error {\n\tc := make(chan *deploy.App)\n\n\t// If the caller explicitly a set of apps, use those\n\t// If they did not, do all apps\n\tif len(apps) == 0 {\n\t\tvar err error\n\t\tapps, err = d.AppNames()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not retrieve list of apps: %v\", err)\n\t\t}\n\t}\n\n\tgo d.Apps(c, apps)\n\ti := 0 // number of apps already processed\n\tfor app := range c {\n\t\tif i >= chaosConfig.MaxApps() {\n\t\t\tbreak\n\t\t}\n\n\t\ti++\n\n\t\tcfg, err := getter.Get(app.Name())\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"WARNING: Could not retrieve config for app=%s. %s\", app.Name(), err)\n\t\t\tcontinue\n\t\t}\n\t\tdoScheduleApp(s, app, *cfg, chaosConfig)\n\t}\n\n\treturn nil\n}\n\n// Add schedules a termination for group at time tm\nfunc (s *Schedule) Add(tm time.Time, group grp.InstanceGroup) {\n\ts.entries = append(s.entries, Entry{Group: group, Time: tm})\n}\n\n// Entries returns the list of schedule entries\nfunc (s *Schedule) Entries() []Entry {\n\treturn s.entries\n}\n\n// doScheduleApp populates the termination schedule for one app\nfunc doScheduleApp(schedule *Schedule, app *deploy.App, cfg chaosmonkey.AppConfig, chaosConfig *config.Monkey) {\n\n\tif !cfg.Enabled {\n\t\tlog.Printf(\"app=%s disabled\\n\", app.Name())\n\t\treturn\n\t}\n\n\tr := rand.New(rand.NewSource(time.Now().UnixNano()))\n\tstartHour := chaosConfig.StartHour()\n\tendHour := chaosConfig.EndHour()\n\tlocation, err := chaosConfig.Location()\n\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"Could not get Location for time zone calculation: %s\", err.Error()))\n\t}\n\n\tgroups := app.EligibleInstanceGroups(cfg)\n\n\tif len(groups) == 0 {\n\t\tlog.Printf(\"app=%s no eligible instance groups\", app.Name())\n\t}\n\n\tfor _, group := range groups {\n\t\tkill := shouldKillInstance(cfg.MeanTimeBetweenKillsInWorkDays, r)\n\t\tlog.Printf(\"%s mtbk=%d kill=%t\\n\", grp.String(group), cfg.MeanTimeBetweenKillsInWorkDays, kill)\n\t\tif kill {\n\t\t\ttime := chooseTerminationTime(time.Now(), startHour, endHour, location)\n\t\t\tschedule.Add(time, group)\n\t\t}\n\t}\n}\n\n// chooseTerminationTime Randomly selects a time to terminate an instance\n// on the same date as now, between startHour:00 and endHour:00 in the same\n// timezone as location\n// Panics if endHour <= startHour\n//\n// Note that there is no guarantee that the selected termination time will be in\n// the future\n//\n// now is passed as an argument to simplify testing\nfunc chooseTerminationTime(now time.Time, startHour int, endHour int, location *time.Location) time.Time {\n\tif endHour <= startHour {\n\t\tpanic(fmt.Sprintf(\"ChooseTermination called with startHour <= endHour, startHour: %d. endHour: %d\", startHour, endHour))\n\t}\n\n\t// Compute the number of minutes in the interval between start and end,\n\t// pick a random one in there, and then add it to the start time as an\n\t// offset\n\tminutesInTimeInterval := (endHour - startHour) * 60\n\tr := rand.New(rand.NewSource(time.Now().UnixNano()))\n\tsample := r.Intn(minutesInTimeInterval)\n\n\t// Convert the sample to duration in minutes\n\toffset := time.Duration(sample) * time.Minute\n\n\tyear, month, day := now.Date()\n\tstartTime := time.Date(year, month, day, startHour, 0, 0, 0, location)\n\n\treturn startTime.Add(offset)\n}\n\n// float64Rand generates random floats on [0, 1)\ntype float64Rand interface {\n\n\t// Return a random float64 on [0, 1)\n\tFloat64() float64\n}\n\n// ShouldKillInstance randomly determines whether an instance should\n// be terminated today by flipping a biased coin.\n//\n// It uses the meanTimeBetweenKillsInWorkDays to determine the probability\n// of a kill\nfunc shouldKillInstance(meanTimeBetweenKillsInWorkDays int, r float64Rand) bool {\n\n\tif meanTimeBetweenKillsInWorkDays <= 0 {\n\t\tpanic(\"meanTimeBetweenKillsInWorkDays is zero or negative\")\n\t}\n\n\tvar pkill = 1.0 / float64(meanTimeBetweenKillsInWorkDays)\n\n\t// Sample uniformly over [0,1)\n\tsample := r.Float64()\n\n\treturn pkill >= sample\n\n}\n\n// Entry is an entry a termination schedule.\n// It contains the instance group that the terminator will randomly select from\n// as well as the time of termination.\ntype Entry struct {\n\tGroup grp.InstanceGroup `json:\"group\"`\n\tTime  time.Time         `json:\"time\"`\n}\n\n// apiGroup represents group representation passed by the API\ntype apiGroup struct {\n\tApp, Account, Region, Stack, Cluster string\n}\n\n// UnmarshalJSON implements Unmarshaler.UnmarshalJSON\nfunc (e *Entry) UnmarshalJSON(b []byte) (err error) {\n\n\tvar ce struct {\n\t\tGroup apiGroup\n\t\tTime  time.Time\n\t}\n\n\terr = json.Unmarshal(b, &ce)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tg := &ce.Group\n\te.Group = grp.New(g.App, g.Account, g.Region, g.Stack, g.Cluster)\n\te.Time = ce.Time\n\treturn nil\n\n}\n\n// Equal checks that two entries are equal\nfunc (e *Entry) Equal(o *Entry) bool {\n\treturn grp.Equal(e.Group, o.Group) && e.Time.Equal(o.Time)\n}\n\n// Crontab returns a termination command for the Entry, in crontab format.\n// It takes as arguments:\n//   - the path to the termination executable\n//   - the account that should execute the job\n//\n// The returned string is not terminated by a newline.\nfunc (e *Entry) Crontab(termPath, account string) string {\n\t// From https://en.wikipedia.org/wiki/Cron\n\t// # * * * * *  account command to execute\n\t// # │ │ │ │ │\n\t// # │ │ │ │ │\n\t// # │ │ │ │ └───── day of week (0 - 6) (0 to 6 are Sunday to Saturday, or use names; 7 is Sunday, the same as 0)\n\t// # │ │ │ └────────── month (1 - 12)\n\t// # │ │ └─────────────── day of month (1 - 31)\n\t// # │ └──────────────────── hour (0 - 23)\n\t// # └───────────────────────── min (0 - 59)\n\tt := e.Time.UTC()\n\treturn fmt.Sprintf(\"%d %d %d %d %d %s %s\", t.Minute(), t.Hour(), t.Day(), t.Month(), t.Weekday(), account, terminateCommand(termPath, e.Group))\n}\n\n// terminateCommand returns the string for terminating an instance\n// given the path to the chaosmonkey termination executable and an instance to terminate\nfunc terminateCommand(termPath string, group grp.InstanceGroup) string {\n\tcmd := fmt.Sprintf(\"%s %s %s\", termPath, group.App(), group.Account())\n\tif cluster, ok := group.Cluster(); ok {\n\t\tcmd = fmt.Sprintf(\"%s --cluster=%s\", cmd, cluster)\n\t}\n\n\tif stack, ok := group.Stack(); ok {\n\t\tcmd = fmt.Sprintf(\"%s --stack=%s\", cmd, stack)\n\t}\n\n\tif region, ok := group.Region(); ok {\n\t\tcmd = fmt.Sprintf(\"%s --region=%s\", cmd, region)\n\t}\n\n\treturn cmd\n}\n\n// logRedirect returns a string to append to a shell command so it redirects\n// stdout and stderr to a logfile\n// Example output: \">> /path/to/log 2>&1\"\nfunc logRedirect(logPath string) string {\n\treturn fmt.Sprintf(\">> %s 2>&1\", logPath)\n}\n\n// Schedule is a collection of termination entries.\ntype Schedule struct {\n\tentries []Entry\n}\n\n// New returns a new Schedule\nfunc New() *Schedule {\n\treturn &Schedule{\n\t\t// We need a zero-element slice instead of a nil slice so that\n\t\t// it will JSON-marshall into '[ ]' instead of 'null'\n\t\tmake([]Entry, 0),\n\t}\n}\n\n// ByTime implements sort.Interface for []Entry based on the time field\ntype ByTime []Entry\n\nfunc (t ByTime) Len() int           { return len(t) }\nfunc (t ByTime) Swap(i, j int)      { t[i], t[j] = t[j], t[i] }\nfunc (t ByTime) Less(i, j int) bool { return t[i].Time.Before(t[j].Time) }\n\n// Crontab returns a schedule of termination commands in crontab format\n// It takes as arguments:\n//   - the path to the executable that terminates an instance\n//   - the account that should execute the job\nfunc (s Schedule) Crontab(exPath string, account string) []byte {\n\tvar result bytes.Buffer\n\n\t// In-place sort the entries before generating the table\n\tsort.Sort(ByTime(s.entries))\n\n\tfor _, entry := range s.entries {\n\t\t_, err := result.WriteString(entry.Crontab(exPath, account))\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"Could not generate string with crontab: %s\", err.Error()))\n\t\t}\n\t\t_, err = result.WriteString(\"\\n\")\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"Could not generate string with crontab: %s\", err.Error()))\n\t\t}\n\n\t}\n\treturn result.Bytes()\n}\n\n// MarshalJSON implements Marshaler.MarshalJSON\nfunc (s Schedule) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(s.entries)\n}\n\n// UnmarshalJSON implements Unmarshaler.UnmarshalJSON\nfunc (s *Schedule) UnmarshalJSON(b []byte) (err error) {\n\treturn json.Unmarshal(b, &s.entries)\n}\n"
  },
  {
    "path": "schedule/schedule_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage schedule_test\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/config/param\"\n\t\"github.com/Netflix/chaosmonkey/v2/mock\"\n\t\"github.com/Netflix/chaosmonkey/v2/schedule\"\n)\n\nfunc TestPopulate(t *testing.T) {\n\t// Setup\n\ts := schedule.New()\n\t// mock deployment returns 4 single-cluster apps, 3 in prod and one in test\n\td := mock.Dep()\n\n\t// mockConfigGetter configures each app for App-level grouping\n\tgetter := new(mockConfigGetter)\n\n\tcfg := config.Defaults()\n\tcfg.Set(param.ScheduleEnabled, true)\n\n\t// Code under test\n\terr := s.Populate(d, getter, cfg, nil)\n\n\tif err != nil {\n\t\tt.Fatalf(\"%v\", err)\n\t}\n\n\t// Assertions\n\texpectedCount := 4\n\n\tdontCare := \"dontcare\"\n\tactualCount := countEntries(s.Crontab(dontCare, dontCare))\n\n\tif actualCount != expectedCount {\n\t\tt.Errorf(\"\\nExpected:\\n%d\\nActual:\\n%d\", expectedCount, actualCount)\n\t}\n\n}\n\n// mockConfigGetter implements chaosmonkey.Getter\n// returns configs for apps\ntype mockConfigGetter struct {\n}\n\n// Get implements chaosmonkey.Getter.Get\n// Configures each app for app-level grouping\n// configures mean time between work days to 1, which ensures\n// a kill on each day\nfunc (g mockConfigGetter) Get(app string) (*chaosmonkey.AppConfig, error) {\n\tcfg := chaosmonkey.NewAppConfig(nil)\n\tcfg.Grouping = chaosmonkey.App\n\tcfg.MeanTimeBetweenKillsInWorkDays = 1\n\treturn &cfg, nil\n}\n\n// countEntries counts the number of entries in a cron file's contents\nfunc countEntries(buf []byte) int {\n\treturn bytes.Count(buf, []byte(\"\\n\"))\n}\n"
  },
  {
    "path": "spinnaker/config.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage spinnaker\n\nimport (\n\t\"io/ioutil\"\n\t\"net/http\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Get implements chaosmonkey.Getter.Get\nfunc (s Spinnaker) Get(app string) (c *chaosmonkey.AppConfig, err error) {\n\t// avoid expanding the response to avoid unneeded load\n\turl := s.appURL(app) + \"?expand=false\"\n\tresp, err := s.client.Get(url)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"http get failed at %s\", url)\n\t}\n\n\tdefer func() {\n\t\tif cerr := resp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = errors.Wrapf(err, \"body close failed at %s\", url)\n\t\t}\n\t}()\n\n\t// should return a 200\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errors.Errorf(\"unexpected response code (%d) from %s\", resp.StatusCode, url)\n\t}\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"body read failed at %s\", url)\n\t}\n\n\treturn fromJSON(body)\n}\n"
  },
  {
    "path": "spinnaker/fromjson.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage spinnaker\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// FromJSON takes a Spinnaker JSON representation of an app\n// and returns a Chaos Monkey config\n// Example:\n//\n//\t{\n//\t    \"name\": \"abc\",\n//\t    \"attributes\": {\n//\t      \"chaosMonkey\": {\n//\t      \"enabled\": true,\n//\t        \"meanTimeBetweenKillsInWorkDays\": 5,\n//\t        \"minTimeBetweenKillsInWorkDays\": 1,\n//\t        \"grouping\": \"cluster\",\n//\t        \"regionsAreIndependent\": false,\n//\t      },\n//\t      \"exceptions\" : [\n//\t          {\n//\t              \"account\": \"test\",\n//\t              \"stack\": \"*\",\n//\t              \"cluster\": \"*\",\n//\t              \"region\": \"*\"\n//\t          },\n//\t          {\n//\t              \"account\": \"prod\",\n//\t              \"stack\": \"*\",\n//\t              \"cluster\": \"*\",\n//\t              \"region\": \"eu-west-1\"\n//\t          },\n//\t      ]\n//\t    }\n//\t}\n//\n// Example of disabled app:\n//\n//\t{\n//\t    \"name\": \"abc\",\n//\t    \"attributes\": {\n//\t      \"chaosMonkey\": {\n//\t      \"enabled\": false\n//\t      }\n//\t    }\n//\t }\n//\n// Example with whitelist\n//\n//\t\t  {\n//\t \t  \"enabled\": true,\n//\t \t  \"grouping\": \"app\",\n//\t \t  \"meanTimeBetweenKillsInWorkDays\": 4,\n//\t \t  \"minTimeBetweenKillsInWorkDays\": 1,\n//\t \t  \"regionsAreIndependent\": true,\n//\t \t  \"exceptions\": [\n//\t \t  \t{\n//\t \t  \t\"account\": \"prod\",\n//\t \t  \t\"region\": \"us-west-2\",\n//\t \t  \t\"stack\": \"foo\",\n//\t \t  \t\"detail\": \"bar\"\n//\t \t  \t}\n//\t \t  ],\n//\t \t  \"whitelist\": [\n//\t \t  \t{\n//\t \t  \t\"account\": \"test\",\n//\t \t  \t\"stack\": \"*\",\n//\t \t  \t\"region\": \"*\",\n//\t \t  \t\"detail\": \"*\"\n//\t \t  \t}\n//\t \t  ]\n//\t\t  }\nfunc fromJSON(js []byte) (*chaosmonkey.AppConfig, error) {\n\tparsed := new(parsedJSON)\n\terr := json.Unmarshal(js, parsed)\n\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"json unmarshal failed\")\n\t}\n\n\tif parsed.Attributes == nil {\n\t\treturn nil, errors.New(\"'attributes' field missing\")\n\t}\n\n\tif parsed.Attributes.ChaosMonkey == nil {\n\t\treturn nil, errors.New(\"'attributes.chaosMonkey' field missing\")\n\t}\n\n\tcm := parsed.Attributes.ChaosMonkey\n\n\tif cm.Enabled == nil {\n\t\treturn nil, errors.New(\"'attributes.chaosMonkey.enabled' field missing\")\n\t}\n\n\t// Check if mean time between kills is missing.\n\t// If not enabled, it's ok if it's missing\n\tif *cm.Enabled && cm.MeanTimeBetweenKillsInWorkDays == nil {\n\t\treturn nil, errors.New(\"attributes.chaosMonkey.meanTimeBetweenKillsInWorkDays missing\")\n\t}\n\n\tif *cm.Enabled && cm.MinTimeBetweenKillsInWorkDays == nil {\n\t\treturn nil, errors.New(\"attributes.chaosMonkey.minTimeBetweenKillsInWorkDays missing\")\n\t}\n\n\tif *cm.Enabled && (*cm.MeanTimeBetweenKillsInWorkDays <= 0) {\n\t\treturn nil, fmt.Errorf(\"invalid attributes.chaosMonkey.meanTimeBetweenKillsInWorkDays: %d\", cm.MeanTimeBetweenKillsInWorkDays)\n\t}\n\n\tgrouping := chaosmonkey.Cluster\n\n\tswitch cm.Grouping {\n\tcase \"app\":\n\t\tgrouping = chaosmonkey.App\n\tcase \"stack\":\n\t\tgrouping = chaosmonkey.Stack\n\tcase \"cluster\":\n\t\tgrouping = chaosmonkey.Cluster\n\tdefault:\n\t\t// If not enabled, the user may not have specified a grouping at all,\n\t\t// in which case we stick with the default\n\t\tif *cm.Enabled {\n\t\t\treturn nil, errors.Errorf(\"Unknown grouping: %s\", cm.Grouping)\n\t\t}\n\t}\n\n\tvar meanTime int\n\tvar minTime int\n\n\tif cm.MeanTimeBetweenKillsInWorkDays != nil {\n\t\tmeanTime = *cm.MeanTimeBetweenKillsInWorkDays\n\t}\n\n\tif cm.MinTimeBetweenKillsInWorkDays != nil {\n\t\tminTime = *cm.MinTimeBetweenKillsInWorkDays\n\t}\n\n\t// Exceptions must have a non-blank region field\n\tfor _, exception := range cm.Exceptions {\n\t\tif exception.Account == \"\" {\n\t\t\treturn nil, errors.New(\"missing account field in exception\")\n\t\t}\n\n\t\tif exception.Region == \"\" {\n\t\t\treturn nil, errors.New(\"missing region field in exception\")\n\t\t}\n\t}\n\n\tcfg := chaosmonkey.AppConfig{\n\t\tEnabled:                        *cm.Enabled,\n\t\tRegionsAreIndependent:          cm.RegionsAreIndependent,\n\t\tGrouping:                       grouping,\n\t\tMeanTimeBetweenKillsInWorkDays: meanTime,\n\t\tMinTimeBetweenKillsInWorkDays:  minTime,\n\t\tExceptions:                     cm.Exceptions,\n\t\tWhitelist:                      cm.Whitelist,\n\t}\n\n\treturn &cfg, nil\n}\n\n// parsedJson is the parsed JSON representation\ntype parsedJSON struct {\n\tName       string      `json:\"name\"`\n\tAttributes *parsedAttr `json:\"attributes\"`\n}\n\ntype parsedAttr struct {\n\tChaosMonkey *parsedChaosMonkey `json:\"chaosmonkey\"`\n}\n\ntype parsedChaosMonkey struct {\n\tEnabled                        *bool                    `json:\"enabled\"`\n\tGrouping                       string                   `json:\"grouping\"`\n\tMeanTimeBetweenKillsInWorkDays *int                     `json:\"meanTimeBetweenKillsInWorkDays\"`\n\tMinTimeBetweenKillsInWorkDays  *int                     `json:\"minTimeBetweenKillsInWorkDays\"`\n\tRegionsAreIndependent          bool                     `json:\"regionsAreIndependent\"`\n\tExceptions                     []chaosmonkey.Exception  `json:\"exceptions\"`\n\tWhitelist                      *[]chaosmonkey.Exception `json:\"whitelist\"`\n}\n"
  },
  {
    "path": "spinnaker/fromjson_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage spinnaker\n\nimport (\n\t\"testing\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n)\n\nfunc TestFromJSON(t *testing.T) {\n\tinput := `\n\t{\n\t\t  \"name\": \"abc\",\n\t\t  \"attributes\": {\n\t\t\t  \"chaosMonkey\": {\n\t\t\t\t  \"enabled\": true,\n\t\t\t\t  \"meanTimeBetweenKillsInWorkDays\": 5,\n\t\t\t\t  \"minTimeBetweenKillsInWorkDays\": 1,\n\t\t\t\t  \"grouping\": \"cluster\",\n\t\t\t\t  \"regionsAreIndependent\": true,\n\t\t\t\t  \"exceptions\" : [\n\t\t\t\t  {\n\t\t\t\t\t  \"account\": \"test\",\n\t\t\t\t\t  \"stack\": \"*\",\n\t\t\t\t\t  \"detail\": \"*\",\n\t\t\t\t\t  \"region\": \"*\"\n\t\t\t\t  },\n\t\t\t\t  {\n\t\t\t\t\t  \"account\": \"prod\",\n\t\t\t\t\t  \"stack\": \"*\",\n\t\t\t\t\t  \"detail\": \"*\",\n\t\t\t\t\t  \"region\": \"eu-west-1\"\n\t\t\t\t  }\n\t\t\t\t  ]\n\t\t\t  }\n\t\t  }\n\t  }\n  `\n\tactual, err := fromJSON([]byte(input))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !actual.Enabled {\n\t\tt.Error(\"Expected enabled to be true\")\n\t}\n\n\tif actual.MeanTimeBetweenKillsInWorkDays != 5 {\n\t\tt.Errorf(\"Expected mean time: 5. acutal mean time: %d\", actual.MeanTimeBetweenKillsInWorkDays)\n\t}\n\n\tif !actual.RegionsAreIndependent {\n\t\tt.Error(\"Expected regions to be independent\")\n\t}\n\n\tif actual.Grouping != chaosmonkey.Cluster {\n\t\tt.Errorf(\"Expected grouping to be Cluster, was %s\", actual.Grouping)\n\t}\n\n\texpectedEx := []chaosmonkey.Exception{\n\t\t{Account: \"test\", Stack: \"*\", Detail: \"*\", Region: \"*\"},\n\t\t{Account: \"prod\", Stack: \"*\", Detail: \"*\", Region: \"eu-west-1\"},\n\t}\n\n\tactualEx := actual.Exceptions\n\n\tif len(actualEx) != len(expectedEx) {\n\t\tt.Fatalf(\"Expected number of exceptions: %d. Actual number of exceptions: %d\", len(expectedEx), len(actualEx))\n\t}\n\n\tif actual.Whitelist != nil {\n\t\tt.Fatalf(\"Expected whitelist to be nil when not specified, was: %v\", actual.Whitelist)\n\t}\n\n\tfor i := range expectedEx {\n\t\tvar expected, actual string\n\t\texpected = expectedEx[i].Account\n\t\tactual = actualEx[i].Account\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"i: %d. Expected account: %s. Actual account: %s\", i, expected, actual)\n\t\t}\n\n\t\texpected = expectedEx[i].Stack\n\t\tactual = actualEx[i].Stack\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"i: %d. Expected stack: %s. Actual stack: %s\", i, expected, actual)\n\t\t}\n\n\t\texpected = expectedEx[i].Detail\n\t\tactual = actualEx[i].Detail\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"i: %d. Expected detail: %s. Actual detail: %s\", i, expected, actual)\n\t\t}\n\n\t\texpected = expectedEx[i].Region\n\t\tactual = actualEx[i].Region\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"i: %d. Expected region: %s. Actual region: %s\", i, expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestFromJSONDisabled(t *testing.T) {\n\tinput := `\n\t{\n\t\t\"name\": \"abc\",\n\t\t\"attributes\": {\n\t\t\t\"chaosMonkey\": {\n\t\t\t\t\"enabled\": false\n\t\t\t}\n\t\t}\n\t}\n\t`\n\n\tactual, err := fromJSON([]byte(input))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif actual.Enabled {\n\t\tt.Error(\"Expected enabled to be false\")\n\t}\n}\n\nfunc TestBadJSON(t *testing.T) {\n\ttests := []string{\n\t\t`{}`,\n\t\t`{\"name\": \"abc\"}`,\n\t\t`{\"name\": \"abc\", \"attributes\": {}}`,\n\t\t`{\"name\": \"abc\", \"attributes\": {\"chaosMonkey\": {}}}`,\n\t\t`{\"name\": \"abc\", \"attributes\": {\"chaosMonkey\": {}}}`,\n\t\t`{\"name\": \"abc\", \"attributes\": {\"chaosMonkey\": {\"enabled\": true}}}`, // if enabled, need valid grouping, mean, and min time.\n\t\t`{\"name\": \"abc\", \"attributes\": {\"chaosMonkey\": {\"enabled\": true, \"grouping\": app}}}`,\n\t\t`{\"name\": \"abc\", \"attributes\": {\"chaosMonkey\": {\"enabled\": true, \"grouping\": app, \"meanTimeBetweenKillsInWorkDays\": 1}}}`,\n\t\t`{\"name\": \"abc\", \"attributes\": {\"chaosMonkey\": {\"enabled\": true, \"grouping\": app, \"minTimeBetweenKillsInWorkDays\": 1}}}`,\n\t\t// mean time must be > 0\n\t\t`{\"name\": \"abc\", \"attributes\": {\"chaosMonkey\": {\"enabled\": true, \"grouping\": \"app\", \"meanTimeBetweenKillsInWorkDays\": 0, \"minTimeBetweenKillsInWorkDays\": 1}}}`,\n\n\t\t// exceptions must have a region field\n\t\t`\n\t\t{\"name\": \"abc\",\n\t\t \"attributes\": {\n\t\t\t\"chaosMonkey\": {\n\t\t\t\t\"enabled\": true, \"grouping\": \"app\", \"meanTimeBetweenKillsInWorkDays\": 1, \"minTimeBetweenKillsInWorkDays\": 1,\n\t\t\t\t\"exceptions\": [{\"account\": \"prod\"}]\n\t    }}}`,\n\n\t\t// exceptions must have an account field\n\t\t`\n\t\t{\"name\": \"abc\",\n\t\t \"attributes\": {\n\t\t\t\"chaosMonkey\": {\n\t\t\t\t\"enabled\": true, \"grouping\": \"app\", \"meanTimeBetweenKillsInWorkDays\": 1, \"minTimeBetweenKillsInWorkDays\": 1,\n\t\t\t\t\"exceptions\": [{\"region\": \"*\"}]\n\t    }}}`,\n\t}\n\n\tfor _, input := range tests {\n\t\t_, err := fromJSON([]byte(input))\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"Expected an error given missing config: %s\", input)\n\t\t}\n\t}\n}\n\nfunc TestFromJSONEmptyWhitelist(t *testing.T) {\n\tinput := `\n\t  {\n\t\t  \"name\": \"abc\",\n\t\t  \"attributes\": {\n\t\t\t  \"chaosMonkey\": {\n\t\t\t\t  \"enabled\": true,\n\t\t\t\t  \"meanTimeBetweenKillsInWorkDays\": 5,\n\t\t\t\t  \"minTimeBetweenKillsInWorkDays\": 1,\n\t\t\t\t  \"grouping\": \"cluster\",\n\t\t\t\t  \"regionsAreIndependent\": true,\n\t\t\t\t  \"whitelist\": [],\n\t\t\t\t  \"exceptions\" : [\n\t\t\t\t  {\n\t\t\t\t\t  \"account\": \"test\",\n\t\t\t\t\t  \"stack\": \"*\",\n\t\t\t\t\t  \"detail\": \"*\",\n\t\t\t\t\t  \"region\": \"*\"\n\t\t\t\t  },\n\t\t\t\t  {\n\t\t\t\t\t  \"account\": \"prod\",\n\t\t\t\t\t  \"stack\": \"*\",\n\t\t\t\t\t  \"detail\": \"*\",\n\t\t\t\t\t  \"region\": \"eu-west-1\"\n\t\t\t\t  }\n\t\t\t\t  ]\n\t\t\t  }\n\t\t  }\n\t  }\n  `\n\tactual, err := fromJSON([]byte(input))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif actual.Whitelist == nil {\n\t\tt.Fatal(\"Whitelist is not present\")\n\t}\n\n\twl := *actual.Whitelist\n\tif len(wl) != 0 {\n\t\tt.Errorf(\"Expected whitelist to be empty, was: %v\", wl)\n\t}\n}\n\nfunc TestFromJSONPopulatedWhitelist(t *testing.T) {\n\tinput := `\n\t  {\n\t\t  \"name\": \"abc\",\n\t\t  \"attributes\": {\n\t\t\t  \"chaosMonkey\": {\n\t\t\t\t  \"enabled\": true,\n\t\t\t\t  \"meanTimeBetweenKillsInWorkDays\": 5,\n\t\t\t\t  \"minTimeBetweenKillsInWorkDays\": 1,\n\t\t\t\t  \"grouping\": \"cluster\",\n\t\t\t\t  \"regionsAreIndependent\": true,\n\t\t\t\t  \"exceptions\": [],\n\t\t\t\t  \"whitelist\" : [\n\t\t\t\t  {\n\t\t\t\t\t  \"account\": \"test\",\n\t\t\t\t\t  \"stack\": \"*\",\n\t\t\t\t\t  \"detail\": \"*\",\n\t\t\t\t\t  \"region\": \"*\"\n\t\t\t\t  },\n\t\t\t\t  {\n\t\t\t\t\t  \"account\": \"prod\",\n\t\t\t\t\t  \"stack\": \"*\",\n\t\t\t\t\t  \"detail\": \"*\",\n\t\t\t\t\t  \"region\": \"eu-west-1\"\n\t\t\t\t  }\n\t\t\t\t  ]\n\t\t\t  }\n\t\t  }\n\t  }\n  `\n\tactual, err := fromJSON([]byte(input))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif actual.Whitelist == nil {\n\t\tt.Fatal(\"Whitelist is not present\")\n\t}\n\n\tactualWl := *actual.Whitelist\n\n\texpectedWl := []chaosmonkey.Exception{\n\t\t{Account: \"test\", Stack: \"*\", Detail: \"*\", Region: \"*\"},\n\t\t{Account: \"prod\", Stack: \"*\", Detail: \"*\", Region: \"eu-west-1\"},\n\t}\n\n\tif len(actualWl) != len(expectedWl) {\n\t\tt.Fatalf(\"Expected whitelist size: %d. Actual whitelist size: %d\", len(expectedWl), len(actualWl))\n\t}\n\n\tfor i := range expectedWl {\n\t\tvar expected, actual string\n\t\texpected = expectedWl[i].Account\n\t\tactual = actualWl[i].Account\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"i: %d. Expected account: %s. Actual account: %s\", i, expected, actual)\n\t\t}\n\n\t\texpected = expectedWl[i].Stack\n\t\tactual = actualWl[i].Stack\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"i: %d. Expected stack: %s. Actual stack: %s\", i, expected, actual)\n\t\t}\n\n\t\texpected = expectedWl[i].Detail\n\t\tactual = actualWl[i].Detail\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"i: %d. Expected detail: %s. Actual detail: %s\", i, expected, actual)\n\t\t}\n\n\t\texpected = expectedWl[i].Region\n\t\tactual = actualWl[i].Region\n\t\tif expected != actual {\n\t\t\tt.Errorf(\"i: %d. Expected region: %s. Actual region: %s\", i, expected, actual)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "spinnaker/spinnaker.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package spinnaker provides an interface to the Spinnaker API\npackage spinnaker\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"golang.org/x/crypto/pkcs12\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\tD \"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n)\n\n// Spinnaker implements the deploy.Deployment interface by querying Spinnaker\n// and the chaosmonkey.Termination interface by terminating via Spinnaker API\n// calls\ntype Spinnaker struct {\n\tendpoint string\n\tclient   *http.Client\n\tuser     string\n}\n\n// spinnakerClusters maps account name (e.g., \"prod\", \"test\") to a list\n// of cluster names\ntype spinnakerClusters map[string][]string\n\n// spinnakerServerGroup represents an autoscaling group, also called a server group,\n// as represented by Spinnaker API\ntype spinnakerServerGroup struct {\n\tName      string\n\tRegion    string\n\tDisabled  bool\n\tInstances []spinnakerInstance\n}\n\n// spinnakerInstance represents an instance as represented by Spinnaker API\ntype spinnakerInstance struct {\n\tName string\n}\n\n// getClient takes PKCS#12 data (encrypted cert data in .p12 format) and the\n// password for the encrypted cert, and returns an http client that does TLS client auth\nfunc getClient(pfxData []byte, password string) (*http.Client, error) {\n\tblocks, err := pkcs12.ToPEM(pfxData, password)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"pkcs.ToPEM failed\")\n\t}\n\n\t// The first block is the cert and the last block is the private key\n\tcertPEMBlock := pem.EncodeToMemory(blocks[0])\n\tkeyPEMBlock := pem.EncodeToMemory(blocks[len(blocks)-1])\n\n\tcert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"tls.X509KeyPair failed\")\n\t}\n\n\ttlsConfig := &tls.Config{\n\t\tCertificates: []tls.Certificate{cert},\n\t}\n\ttransport := &http.Transport{TLSClientConfig: tlsConfig}\n\treturn &http.Client{Transport: transport}, nil\n}\n\n// getClientX509 takes X509 data (Public and Private keys) and the\n// and returns an http client that does TLS client auth\nfunc getClientX509(x509Cert, x509Key string) (*http.Client, error) {\n\tcert, err := tls.LoadX509KeyPair(x509Cert, x509Key)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"tls.X509KeyPair failed\")\n\t}\n\ttlsConfig := &tls.Config{\n\t\tCertificates:       []tls.Certificate{cert},\n\t\tInsecureSkipVerify: true,\n\t}\n\ttransport := &http.Transport{TLSClientConfig: tlsConfig}\n\treturn &http.Client{Transport: transport}, nil\n}\n\n// NewFromConfig returns a Spinnaker based on config\nfunc NewFromConfig(cfg *config.Monkey) (Spinnaker, error) {\n\tspinnakerEndpoint := cfg.SpinnakerEndpoint()\n\tcertPath := cfg.SpinnakerCertificate()\n\tencryptedPassword := cfg.SpinnakerEncryptedPassword()\n\tuser := cfg.SpinnakerUser()\n\tx509Cert := cfg.SpinnakerX509Cert()\n\tx509Key := cfg.SpinnakerX509Key()\n\n\tif spinnakerEndpoint == \"\" {\n\t\treturn Spinnaker{}, errors.New(\"FATAL: no spinnaker endpoint specified in config\")\n\t}\n\n\tvar password string\n\tvar err error\n\tvar decryptor chaosmonkey.Decryptor\n\n\tif encryptedPassword != \"\" {\n\t\tdecryptor, err = deps.GetDecryptor(cfg)\n\t\tif err != nil {\n\t\t\treturn Spinnaker{}, err\n\t\t}\n\n\t\tpassword, err = decryptor.Decrypt(encryptedPassword)\n\t\tif err != nil {\n\t\t\treturn Spinnaker{}, err\n\t\t}\n\t}\n\n\treturn New(spinnakerEndpoint, certPath, password, x509Cert, x509Key, user)\n\n}\n\n// New returns a Spinnaker using a .p12 cert at certPath encrypted with\n// password or x509 cert. The user argument identifies the email address of the user which is\n// sent in the payload of the terminateInstances task API call\nfunc New(endpoint string, certPath string, password string, x509Cert string, x509Key string, user string) (Spinnaker, error) {\n\tvar client *http.Client\n\tvar err error\n\n\tif x509Cert != \"\" && certPath != \"\" {\n\t\treturn Spinnaker{}, errors.New(\"cannot use both p12 and x509 certs, choose one\")\n\t}\n\n\tif certPath != \"\" {\n\t\tpfxData, err := ioutil.ReadFile(certPath)\n\t\tif err != nil {\n\t\t\treturn Spinnaker{}, errors.Wrapf(err, \"failed to read file %s\", certPath)\n\t\t}\n\n\t\tclient, err = getClient(pfxData, password)\n\t\tif err != nil {\n\t\t\treturn Spinnaker{}, err\n\t\t}\n\t} else if x509Cert != \"\" {\n\t\tclient, err = getClientX509(x509Cert, x509Key)\n\t\tif err != nil {\n\t\t\treturn Spinnaker{}, err\n\t\t}\n\t} else {\n\t\tclient = new(http.Client)\n\t}\n\n\treturn Spinnaker{endpoint: endpoint, client: client, user: user}, nil\n}\n\n// AccountID returns numerical ID associated with an AWS account\nfunc (s Spinnaker) AccountID(name string) (id string, err error) {\n\turl := s.accountURL(name)\n\n\tresp, err := s.client.Get(url)\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"could not retrieve account info for %s from spinnaker url %s\", name, url)\n\t}\n\n\tdefer func() {\n\t\tif cerr := resp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = errors.Wrapf(err, \"failed to close response body from %s\", url)\n\t\t}\n\t}()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"failed to read body from url %s\", url)\n\t}\n\n\tvar info struct {\n\t\tAccountID string `json:\"accountId\"`\n\t\tError     string `json:\"error\"`\n\t}\n\n\terr = json.Unmarshal(body, &info)\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"could not parse body of %s as json, body: %s, error\", url, body)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tif info.Error == \"\" {\n\t\t\treturn \"\", errors.Errorf(\"%s returned unexpected status code: %d, body: %s\", url, resp.StatusCode, body)\n\t\t}\n\n\t\treturn \"\", errors.New(info.Error)\n\t}\n\n\t// Some backends may not have associated account ids\n\tif info.AccountID == \"\" {\n\t\treturn s.alternateAccountID(name)\n\t}\n\n\treturn info.AccountID, nil\n\n}\n\n// alternateAccountID returns an account ID for accounts that don't have their\n// own ids.\nfunc (s Spinnaker) alternateAccountID(name string) (string, error) {\n\n\t// Sanity check: this should never be called with \"prod\" or \"test\" as an\n\t// argument, since this would result in infinite recursion\n\tif name == \"prod\" || name == \"test\" {\n\t\treturn \"\", fmt.Errorf(\"alternateAccountID called with forbidden arg: %s\", name)\n\t}\n\n\t// Heuristic: if account name has \"test\" in the name, we return the \"test\"\n\t// account id, otherwise with  we use the \"prod\" account id\n\tif strings.Contains(name, \"test\") {\n\t\treturn s.AccountID(\"test\")\n\t}\n\n\treturn s.AccountID(\"prod\")\n}\n\n// Apps implements deploy.Deployment.Apps\nfunc (s Spinnaker) Apps(c chan<- *D.App, appNames []string) {\n\t// Close the channel we're done\n\tdefer close(c)\n\n\tfor _, appName := range appNames {\n\t\tapp, err := s.GetApp(appName)\n\t\tif err != nil {\n\t\t\t// If we have a problem with one app, we go to the next one\n\t\t\tlog.Printf(\"WARNING: GetApp failed for %s: %v\", appName, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tc <- app\n\t}\n}\n\n// GetInstanceIDs gets the instance ids for a cluster\nfunc (s Spinnaker) GetInstanceIDs(app string, account D.AccountName, cloudProvider string, region D.RegionName, cluster D.ClusterName) (D.ASGName, []D.InstanceID, error) {\n\turl := s.activeASGURL(app, string(account), string(cluster), cloudProvider, string(region))\n\n\tresp, err := s.client.Get(url)\n\tif err != nil {\n\t\treturn \"\", nil, errors.Wrapf(err, \"http get failed at %s\", url)\n\t}\n\n\tdefer func() {\n\t\tif cerr := resp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = errors.Wrapf(err, \"body close failed at %s\", url)\n\t\t}\n\t}()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", nil, errors.Errorf(\"unexpected response code (%d) from %s\", resp.StatusCode, url)\n\t}\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", nil, errors.Wrap(err, fmt.Sprintf(\"body read failed at %s\", url))\n\t}\n\n\tvar data struct {\n\t\tName      string\n\t\tInstances []struct{ Name string }\n\t}\n\n\terr = json.Unmarshal(body, &data)\n\tif err != nil {\n\t\treturn \"\", nil, errors.Wrapf(err, \"failed to parse json at %s\", url)\n\t}\n\n\tasg := D.ASGName(data.Name)\n\tinstances := make([]D.InstanceID, len(data.Instances))\n\tfor i, instance := range data.Instances {\n\t\tinstances[i] = D.InstanceID(instance.Name)\n\t}\n\n\treturn asg, instances, nil\n\n}\n\n// GetApp implements deploy.Deployment.GetApp\nfunc (s Spinnaker) GetApp(appName string) (*D.App, error) {\n\t// data arg is a map like {accountName: {clusterName: {regionName: {asgName: [instanceId]}}}}\n\tdata := make(D.AppMap)\n\tfor account, clusters := range s.clusters(appName) {\n\t\tcloudProvider, err := s.CloudProvider(account)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"retrieve cloud provider failed\")\n\t\t}\n\t\taccount := D.AccountName(account)\n\t\tdata[account] = D.AccountInfo{\n\t\t\tCloudProvider: cloudProvider,\n\t\t\tClusters:      make(map[D.ClusterName]map[D.RegionName]map[D.ASGName][]D.InstanceID),\n\t\t}\n\t\tfor _, clusterName := range clusters {\n\t\t\tclusterName := D.ClusterName(clusterName)\n\t\t\tdata[account].Clusters[clusterName] = make(map[D.RegionName]map[D.ASGName][]D.InstanceID)\n\t\t\tasgs, err := s.asgs(appName, string(account), string(clusterName))\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"WARNING: could not retrieve asgs for app:%s account:%s cluster:%s : %v\", appName, account, clusterName, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, asg := range asgs {\n\n\t\t\t\t// We don't terminate instances in disabled ASGs\n\t\t\t\tif asg.Disabled {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tregion := D.RegionName(asg.Region)\n\t\t\t\tasgName := D.ASGName(asg.Name)\n\n\t\t\t\t_, present := data[account].Clusters[clusterName][region]\n\t\t\t\tif !present {\n\t\t\t\t\tdata[account].Clusters[clusterName][region] = make(map[D.ASGName][]D.InstanceID)\n\t\t\t\t}\n\n\t\t\t\tdata[account].Clusters[clusterName][region][asgName] = make([]D.InstanceID, len(asg.Instances))\n\n\t\t\t\tfor i, instance := range asg.Instances {\n\t\t\t\t\tdata[account].Clusters[clusterName][region][asgName][i] = D.InstanceID(instance.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn D.NewApp(appName, data), nil\n}\n\n// AppNames returns list of names of all apps\nfunc (s Spinnaker) AppNames() (appnames []string, err error) {\n\turl := s.appsURL()\n\tresp, err := s.client.Get(url)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not retrieve list of apps from spinnaker url %s: %v\", url, err)\n\t}\n\n\tdefer func() {\n\t\tif cerr := resp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = fmt.Errorf(\"failed to close response body from %s: %v\", url, err)\n\t\t}\n\t}()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read body when retrieving spinnaker app names from %s: %v\", url, err)\n\t}\n\tvar apps []spinnakerApp\n\terr = json.Unmarshal(body, &apps)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not parse spinnaker apps list from %s: body: \\\"%s\\\": %v\", url, string(body), err)\n\t}\n\n\tresult := make([]string, len(apps))\n\tfor i, app := range apps {\n\t\tresult[i] = app.Name\n\t}\n\n\treturn result, nil\n\n}\n\n// spinnakerApp returns an app as represented by the Spinnaker API\ntype spinnakerApp struct {\n\tName string\n}\n\n// clusters returns a map from account name to list of cluster names\nfunc (s Spinnaker) clusters(appName string) spinnakerClusters {\n\turl := s.clustersURL(appName)\n\tresp, err := s.client.Get(url)\n\tif err != nil {\n\t\tlog.Println(\"Error connecting to spinnaker clusters endpoint\")\n\t\tlog.Println(url)\n\t\tlog.Fatalln(err)\n\t}\n\n\tdefer func() {\n\t\tif err := resp.Body.Close(); err != nil {\n\t\t\tlog.Printf(\"Error closing response body of %s: %v\", url, err)\n\t\t}\n\t}()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\tlog.Println(\"Error retrieving spinnaker clusters for app\", appName)\n\t\tlog.Println(url)\n\t\tlog.Println(string(body))\n\t\tlog.Fatalln(err)\n\t}\n\n\t// Example cluster output:\n\t/*\n\t\t{\n\t\t  \"prod\": [\n\t\t\t\"abc-prod\"\n\t\t  ],\n\t\t  \"test\": [\n\t\t\t\"abc-beta\"\n\t\t  ]\n\t\t}\n\t*/\n\tvar m spinnakerClusters\n\n\terr = json.Unmarshal(body, &m)\n\tif err != nil {\n\t\tlog.Println(\"Error parsing body when retrieving cluster info for\", appName)\n\t\tlog.Println(url)\n\t\tlog.Println(string(body))\n\t\tlog.Fatalln(err)\n\t}\n\n\treturn m\n}\n\n// asgs returns a slice of autoscaling groups associated with the given cluster\nfunc (s Spinnaker) asgs(appName, account, clusterName string) (result []spinnakerServerGroup, err error) {\n\turl := s.serverGroupsURL(appName, account, clusterName)\n\tresp, err := s.client.Get(url)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to retrieve server groups url (%s): %v\", url, err)\n\t}\n\n\tdefer func() {\n\t\tif cerr := resp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = fmt.Errorf(\"failed to close response body of %s: %v\", url, err)\n\t\t}\n\t}()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read body of server groups url (%s): body: '%s': %v\", url, string(body), err)\n\t}\n\n\t// Example:\n\t/*\n\t\t[\n\t\t  {\n\t\t    \"name\": \"abc-prod-v016\",\n\t\t    \"region\": \"us-east-1\",\n\t\t    \"zones\": [\n\t\t      \"us-east-1c\",\n\t\t      \"us-east-1d\",\n\t\t      \"us-east-1e\"\n\t\t    ],\n\t\t    \"disabled\": false,\n\t\t    \"instances\": [\n\t\t      {\n\t\t        \"name\": \"i-f9ffb752\",\n\t\t\t\t...\n\t\t\t  },\n\t\t\t...\n\t\t   ]\n\t\t  }\n\t\t]\n\t*/\n\n\tvar asgs []spinnakerServerGroup\n\terr = json.Unmarshal(body, &asgs)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse body of spinnaker asgs url (%s): body: '%s'. %v\", url, string(body), err)\n\t}\n\n\treturn asgs, nil\n}\n\n// CloudProvider returns the cloud provider for a given account name\nfunc (s Spinnaker) CloudProvider(name string) (provider string, err error) {\n\taccount, err := s.account(name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif account.CloudProvider == \"\" {\n\t\treturn \"\", errors.New(\"no cloudProvider field in response body\")\n\t}\n\n\treturn account.CloudProvider, nil\n}\n\n// account represents a spinnaker account\ntype account struct {\n\tCloudProvider string `json:\"cloudProvider\"`\n\tName          string `json:\"name\"`\n\tError         string `json:\"error\"`\n}\n\n// account returns an account by its name\nfunc (s Spinnaker) account(name string) (account, error) {\n\turl := s.accountsURL(true)\n\tresp, err := s.client.Get(url)\n\tvar ac account\n\n\t// Usual HTTP checks\n\tif err != nil {\n\t\treturn ac, errors.Wrapf(err, \"http get failed at %s\", url)\n\t}\n\n\tdefer func() {\n\t\tif cerr := resp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = errors.Wrap(err, fmt.Sprintf(\"body close failed at %s\", url))\n\t\t}\n\t}()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn ac, errors.Wrapf(err, \"body read failed at %s\", url)\n\t}\n\n\tvar accounts []account\n\terr = json.Unmarshal(body, &accounts)\n\tif err != nil {\n\t\treturn ac, errors.Wrap(err, \"json unmarshal failed\")\n\t}\n\tstatusKO := resp.StatusCode != http.StatusOK\n\n\t// Finally find account\n\tfor _, a := range accounts {\n\t\tif a.Name != name {\n\t\t\tcontinue\n\t\t}\n\t\tif statusKO {\n\t\t\tif a.Error == \"\" {\n\t\t\t\treturn ac, errors.Errorf(\"unexpected status code: %d. body: %s\", resp.StatusCode, body)\n\t\t\t}\n\n\t\t\treturn ac, errors.Errorf(\"unexpected status code: %d. error: %s\", resp.StatusCode, a.Error)\n\t\t}\n\n\t\treturn a, nil\n\t}\n\n\treturn ac, errors.New(\"the account name doesn't exist\")\n}\n\n// GetClusterNames returns a list of cluster names for an app\nfunc (s Spinnaker) GetClusterNames(app string, account D.AccountName) (clusters []D.ClusterName, err error) {\n\turl := s.appURL(app)\n\tresp, err := s.client.Get(url)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"http get failed at %s\", url)\n\t}\n\n\tdefer func() {\n\t\tif cerr := resp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = errors.Wrapf(err, \"body close failed at %s\", url)\n\t\t}\n\t}()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errors.Errorf(\"unexpected response code (%d) from %s\", resp.StatusCode, url)\n\t}\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, fmt.Sprintf(\"body read failed at %s\", url))\n\t}\n\n\tvar pcl struct {\n\t\tClusters map[D.AccountName][]struct {\n\t\t\tName D.ClusterName\n\t\t}\n\t}\n\n\terr = json.Unmarshal(body, &pcl)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed to parse json at %s\", url)\n\t}\n\n\tcls := pcl.Clusters[account]\n\n\tclusters = make([]D.ClusterName, len(cls))\n\tfor i, cl := range cls {\n\t\tclusters[i] = cl.Name\n\t}\n\n\treturn clusters, nil\n}\n\n// GetRegionNames returns a list of regions that a cluster is deployed into\nfunc (s Spinnaker) GetRegionNames(app string, account D.AccountName, cluster D.ClusterName) ([]D.RegionName, error) {\n\turl := s.clusterURL(app, string(account), string(cluster))\n\tresp, err := s.client.Get(url)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"http get failed at %s\", url)\n\t}\n\n\tdefer func() {\n\t\tif cerr := resp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = errors.Wrapf(err, \"body close failed at %s\", url)\n\t\t}\n\t}()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errors.Errorf(\"unexpected response code (%d) from %s\", resp.StatusCode, url)\n\t}\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, fmt.Sprintf(\"body read failed at %s\", url))\n\t}\n\n\tvar cl struct {\n\t\tServerGroups []struct{ Region D.RegionName }\n\t}\n\n\terr = json.Unmarshal(body, &cl)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed to parse json at %s\", url)\n\t}\n\n\tset := make(map[D.RegionName]bool)\n\tfor _, g := range cl.ServerGroups {\n\t\tset[g.Region] = true\n\t}\n\n\tresult := make([]D.RegionName, 0, len(set))\n\tfor region := range set {\n\t\tresult = append(result, region)\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "spinnaker/terminator.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage spinnaker\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n)\n\nconst terminateType string = \"terminateInstances\"\n\ntype (\n\t// killPayload is the POST request body for Spinnaker instance terminations\n\tkillPayload struct {\n\t\tApplication string  `json:\"application\"`\n\t\tDescription string  `json:\"description\"`\n\t\tJob         []kpJob `json:\"job\"`\n\t}\n\n\t// kpJob is the \"job\" of killPayload\n\tkpJob struct {\n\t\tUser            string   `json:\"user\"`\n\t\tType            string   `json:\"type\"`\n\t\tCredentials     string   `json:\"credentials\"`\n\t\tRegion          string   `json:\"region\"`\n\t\tServerGroupName string   `json:\"serverGroupName\"`\n\t\tInstanceIDs     []string `json:\"instanceIds\"`\n\t\tCloudProvider   string   `json:\"cloudProvider\"`\n\t}\n\n\t// fakeTerminator implements term.Terminator, but it just logs the http requests rather than actually\n\t// making them\n\tfakeTerminator struct{}\n)\n\n// NewFakeTerm returns a fake Terminator that prints out what API calls it would make against Spinnaker\nfunc NewFakeTerm() chaosmonkey.Terminator {\n\treturn fakeTerminator{}\n}\n\n// tasksURL returns the Spinnaker tasks URL associated with an app\nfunc (s Spinnaker) tasksURL(appName string) string {\n\treturn s.appURL(appName) + \"/tasks\"\n}\n\n// Kill implements term.Terminator.Kill\nfunc (t fakeTerminator) Execute(trm chaosmonkey.Termination) error {\n\treturn nil\n}\n\n// Execute implements term.Terminator.Execute\nfunc (s Spinnaker) Execute(trm chaosmonkey.Termination) (err error) {\n\tins := trm.Instance\n\turl := s.tasksURL(ins.AppName())\n\n\totherID, err := s.OtherID(ins)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"retrieve other id failed\")\n\t}\n\n\tpayload := killJSONPayload(ins, otherID, s.user)\n\tresp, err := s.client.Post(url, \"application/json\", bytes.NewReader(payload))\n\tif err != nil {\n\t\treturn errors.Wrap(err, fmt.Sprintf(\"POST to %s failed, (body '%s')\", url, string(payload)))\n\t}\n\n\tdefer func() {\n\t\tif cerr := resp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = errors.Wrap(cerr, fmt.Sprintf(\"failed to close response body of %s\", url))\n\t\t}\n\t}()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tlog.Printf(\"Unexpected response: %d\", resp.StatusCode)\n\t\tcontents, err := ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to read response body\")\n\t\t}\n\t\treturn fmt.Errorf(\"unexpected response code: %d, body: %s\", resp.StatusCode, string(contents))\n\t}\n\n\treturn nil\n}\n\n// killJsonPayload generates the JSON request body for terminating an instance\n// otherID is an optional second instance ID, as some backends may have a second\n// identifer.\nfunc killJSONPayload(ins chaosmonkey.Instance, otherID string, spinnakerUser string) []byte {\n\tvar desc string\n\tif otherID != \"\" {\n\t\tdesc = fmt.Sprintf(\"Chaos Monkey terminate instance: %s %s (%s, %s, %s)\", ins.ID(), otherID, ins.AccountName(), ins.RegionName(), ins.ASGName())\n\t} else {\n\t\tdesc = fmt.Sprintf(\"Chaos Monkey terminate instance: %s (%s, %s, %s)\", ins.ID(), ins.AccountName(), ins.RegionName(), ins.ASGName())\n\t}\n\n\tp := killPayload{\n\t\tApplication: ins.AppName(),\n\t\tDescription: desc,\n\t\tJob: []kpJob{\n\t\t\t{\n\t\t\t\tUser:            spinnakerUser,\n\t\t\t\tType:            terminateType,\n\t\t\t\tCredentials:     ins.AccountName(),\n\t\t\t\tRegion:          ins.RegionName(),\n\t\t\t\tServerGroupName: ins.ASGName(),\n\t\t\t\tInstanceIDs:     []string{ins.ID()},\n\t\t\t\tCloudProvider:   ins.CloudProvider(),\n\t\t\t},\n\t\t},\n\t}\n\n\tresult, err := json.Marshal(p)\n\tif err != nil {\n\t\tlog.Fatalf(\"chronos.jsonPayload could not marshal data into json: %v\", err)\n\t}\n\n\treturn result\n}\n\n// OtherID returns the alternate instance id of an instance, if it exists\n// If there is no alternate instance id, it returns an empty string\n// This is used by Titus, where we also report the uuid\nfunc (s Spinnaker) OtherID(ins chaosmonkey.Instance) (otherID string, err error) {\n\turl := s.instanceURL(ins.AccountName(), ins.RegionName(), ins.ID())\n\tresp, err := s.client.Get(url)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, fmt.Sprintf(\"get failed on %s\", url))\n\t}\n\n\tdefer func() {\n\t\tif cerr := resp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = errors.Wrap(cerr, fmt.Sprintf(\"failed to close response body from %s\", url))\n\t\t}\n\t}()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, fmt.Sprintf(\"body read failed at %s\", url))\n\t}\n\n\t// Example of response body:\n\t/*\n\t\t{\n\t\t\t...\n\t\t\t\"health\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"Titus\",\n\t\t\t\t\t\"healthClass\": \"platform\",\n\t\t\t\t\t\"state\": \"Up\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"instanceId\": \"55fe33ab-5b66-450a-85f7-f3129806b87f\",\n\t\t\t\t\t\"titusTaskId\": \"Titus-123456-worker-0-0\",\n\t\t\t\t\t...\n\t\t\t\t}\n\t\t\t],\n\t\t}\n\t*/\n\n\tvar fields struct {\n\t\tHealth []map[string]interface{} `json:\"health\"`\n\t\tError  string                   `json:\"error\"`\n\t}\n\n\terr = json.Unmarshal(body, &fields)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, fmt.Sprintf(\"json unmarshal failed, body: %s\", body))\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tif fields.Error == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"unexpected status code: %d. body: %s\", resp.StatusCode, body)\n\t\t}\n\n\t\treturn \"\", fmt.Errorf(\"unexpected status code: %d. error: %s\", resp.StatusCode, fields.Error)\n\t}\n\n\t// In some cases, an instance may be missing health information.\n\t// We just return a blank otherID in that case\n\tif len(fields.Health) < 2 {\n\t\treturn \"\", nil\n\t}\n\n\totherID, ok := fields.Health[1][\"instanceId\"].(string)\n\tif !ok {\n\t\treturn \"\", nil\n\t}\n\n\t// If the instance id is the same, there is no alternate\n\tif ins.ID() == otherID {\n\t\treturn \"\", nil\n\t}\n\n\treturn otherID, nil\n}\n"
  },
  {
    "path": "spinnaker/terminator_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage spinnaker\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/mock\"\n)\n\nfunc TestKillJSONPayload(t *testing.T) {\n\tins := mock.Instance{\n\t\tApp:        \"foo\",\n\t\tAccount:    \"test\",\n\t\tStack:      \"beta\",\n\t\tCluster:    \"foo-beta\",\n\t\tRegion:     \"us-west-2\",\n\t\tASG:        \"foo-beta-v052\",\n\t\tInstanceID: \"i-703a0439\",\n\t}\n\n\totherID := \"\" // some backends may have a second instance identifier\n\n\tpayload := killJSONPayload(ins, otherID, \"user@example.com\")\n\n\t/*\n\t\t{\n\t\t  \"application\": \"foo\",\n\t\t  \"description\": \"Chaos Monkey terminate instance: i-703a0439 (test, us-west-2, foo-beta-v052)\",\n\t\t  \"job\": [\n\t\t\t{\n\t\t\t  \"user\": \"user@example.com\"\n\t\t\t  \"type\": \"terminateInstances\",\n\t\t\t  \"credentials\": \"test\",\n\t\t\t  \"region\": \"us-west-2\",\n\t\t\t  \"serverGroupName\": \"foo-beta-v052\",\n\t\t\t  \"instanceIds\": [\n\t\t\t\t\"i-703a0439\"\n\t\t\t  ],\n\t\t\t}\n\t\t  ]\n\t\t}\n\t*/\n\n\tvar f interface{}\n\terr := json.Unmarshal(payload, &f)\n\tif err != nil {\n\t\tt.Log(string(payload))\n\t\tt.Fatal(err)\n\t}\n\n\tm := f.(map[string]interface{})\n\tif m == nil {\n\t\tt.Fatalf(\"payload is not a JSON object: %s\", payload)\n\t}\n\n\ttests := []struct {\n\t\tname, value string\n\t}{\n\t\t{\"application\", \"foo\"},\n\t\t{\"description\", \"Chaos Monkey terminate instance: i-703a0439 (test, us-west-2, foo-beta-v052)\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tif _, ok := m[tt.name].(string); !ok {\n\t\t\tt.Fatalf(\"Missing field: %s\", tt.name)\n\t\t}\n\n\t\tif got, want := m[tt.name].(string), tt.value; got != want {\n\t\t\tt.Errorf(\"got ['%s']=%s, want %s\", tt.name, got, want)\n\t\t}\n\t}\n\n\tvar jobs []interface{}\n\tvar ok bool\n\n\tif jobs, ok = m[\"job\"].([]interface{}); !ok {\n\t\tt.Fatalf(\"jobs is not an array: %s\", payload)\n\t}\n\n\tif got, want := len(jobs), 1; got != want {\n\t\tt.Fatalf(\"got len(jobs)=%d, want: %d\", got, want)\n\t}\n\n\tvar job map[string]interface{}\n\n\tif job, ok = jobs[0].(map[string]interface{}); !ok {\n\t\tt.Fatalf(\"job[0] is not a json object: %s\", payload)\n\t}\n\n\ttests = []struct {\n\t\tname, value string\n\t}{\n\t\t{\"type\", \"terminateInstances\"},\n\t\t{\"serverGroupName\", \"foo-beta-v052\"},\n\t\t{\"region\", \"us-west-2\"},\n\t\t{\"credentials\", \"test\"},\n\t\t{\"user\", \"user@example.com\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tif got, want := job[tt.name].(string), tt.value; got != want {\n\t\t\tt.Errorf(\"got obj['%s']=%s, want %s\", tt.name, got, want)\n\t\t}\n\t}\n\n\tids, ok := job[\"instanceIds\"].([]interface{})\n\tif !ok {\n\t\tt.Fatalf(\"No job.instanceIds field: %s\", payload)\n\t}\n\n\tif len(ids) != 1 {\n\t\tt.Fatalf(\"job.instanceIds field is not 1: %v\", payload)\n\t}\n\n\tid, ok := ids[0].(string)\n\tif !ok {\n\t\tt.Fatalf(\"job.InstanceIds[0] is not a string: %v\", payload)\n\t}\n\n\tif got, want := id, \"i-703a0439\"; got != want {\n\t\tt.Fatalf(\"Wrong instance id. got: %s, want: %s\", got, want)\n\t}\n}\n\nfunc TestKillJSONPayloadWithOtherID(t *testing.T) {\n\tins := mock.Instance{\n\t\tApp:        \"foo\",\n\t\tAccount:    \"other\",\n\t\tStack:      \"beta\",\n\t\tCluster:    \"foo-beta\",\n\t\tRegion:     \"us-west-2\",\n\t\tASG:        \"foo-beta-v052\",\n\t\tInstanceID: \"custom-id-123\",\n\t}\n\n\totherID := \"39033754-c0ac-423d-aab7-2736548acf65\"\n\tpayload := killJSONPayload(ins, otherID, \"user@example.com\")\n\n\tvar f interface{}\n\terr := json.Unmarshal(payload, &f)\n\tif err != nil {\n\t\tt.Log(string(payload))\n\t\tt.Fatal(err)\n\t}\n\n\tm := f.(map[string]interface{})\n\tif m == nil {\n\t\tt.Fatalf(\"payload is not a JSON object: %s\", payload)\n\t}\n\n\twant := \"Chaos Monkey terminate instance: custom-id-123 39033754-c0ac-423d-aab7-2736548acf65 (other, us-west-2, foo-beta-v052)\"\n\n\tif got := m[\"description\"]; got != want {\n\t\tt.Errorf(\"got: %s, want: %s\", got, want)\n\t}\n}\n"
  },
  {
    "path": "spinnaker/urls.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage spinnaker\n\nimport \"fmt\"\n\n// appsUrl returns the Spinnaker endpoint for retrieving all applications\nfunc (s Spinnaker) appsURL() string {\n\treturn s.endpoint + \"/applications\"\n}\n\n// appUrl returns the Spinnaker endpoint for retrieving one application\nfunc (s Spinnaker) appURL(appName string) string {\n\treturn s.endpoint + \"/applications/\" + appName\n}\n\n// clustersUrl returns the Spinnaker endpoint for retrieving applications\nfunc (s Spinnaker) clustersURL(appName string) string {\n\treturn fmt.Sprintf(\"%s/applications/%s/clusters\", s.endpoint, appName)\n}\n\n// clusterUrl returns the Spinnaker endpoint for retrieving info about a cluster\nfunc (s Spinnaker) clusterURL(appName string, account string, clusterName string) string {\n\treturn fmt.Sprintf(\"%s/applications/%s/clusters/%s/%s\", s.endpoint, appName, account, clusterName)\n}\n\n// serverGroupsUrl returns the Spinnaker endpoint for retrieving server groups\nfunc (s Spinnaker) serverGroupsURL(appName, account, clusterName string) string {\n\treturn fmt.Sprintf(\"%s/applications/%s/clusters/%s/%s/serverGroups\", s.endpoint, appName, account, clusterName)\n}\n\n// accountURL returns the Spinnaker endpoint for retrieving account info\nfunc (s Spinnaker) accountURL(account string) string {\n\treturn fmt.Sprintf(\"%s/credentials/%s\", s.endpoint, account)\n}\n\n// accountsURL returns the Spinnaker endpoint for retrieving all accounts, with details or not\nfunc (s Spinnaker) accountsURL(expanded bool) string {\n\tvar qs string\n\tif expanded {\n\t\tqs = \"?expand=true\"\n\t}\n\treturn fmt.Sprintf(\"%s/credentials/\"+qs, s.endpoint)\n}\n\n// instanceURL returns the spinnaker URL for an instance\nfunc (s Spinnaker) instanceURL(account string, region string, id string) string {\n\treturn fmt.Sprintf(\"%s/instances/%s/%s/%s\", s.endpoint, account, region, id)\n}\n\n// activeASGURL returns the spinnaker URL for getting the active asg in a cluster\nfunc (s Spinnaker) activeASGURL(appName, account, clusterName, cloudProvider, region string) string {\n\treturn fmt.Sprintf(\"%s/applications/%s/clusters/%s/%s/%s/%s/serverGroups/target/CURRENT?onlyEnabled=true\",\n\t\ts.endpoint, appName, account, clusterName, cloudProvider, region)\n}\n"
  },
  {
    "path": "term/term.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package term contains the logic for terminating instances\npackage term\n\nimport (\n\t\"log\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n\t\"github.com/Netflix/chaosmonkey/v2/eligible\"\n\t\"github.com/Netflix/chaosmonkey/v2/grp\"\n)\n\ntype leashedKiller struct {\n}\n\nfunc (l leashedKiller) Execute(trm chaosmonkey.Termination) error {\n\tlog.Printf(\"leashed=true, not killing instance %s\", trm.Instance.ID())\n\treturn nil\n}\n\n// UnleashedInTestEnv is an error returned by Terminate if running unleashed in\n// the test environment, which is not allowed\ntype UnleashedInTestEnv struct{}\n\nfunc (err UnleashedInTestEnv) Error() string {\n\treturn \"not terminating: Chaos Monkey may not run unleashed in the test environment\"\n}\n\n// Terminate executes the \"terminate\" command. This selects an instance\n// based on the app, account, region, stack, cluster passed\n//\n// region, stack, and cluster may be blank\nfunc Terminate(d deps.Deps, app string, account string, region string, stack string, cluster string) error {\n\tenabled, err := d.MonkeyCfg.Enabled()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"not terminating: could not determine if monkey is enabled\")\n\t}\n\n\tif !enabled {\n\t\tlog.Println(\"not terminating: enabled=false\")\n\t\treturn nil\n\t}\n\n\tproblem, err := d.Ou.Outage()\n\n\t// If the check for ongoing outage fails, we err on the safe side nd don't terminate an instance\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"not terminating: problem checking if there is an outage\")\n\t}\n\n\tif problem {\n\t\tlog.Println(\"not terminating: outage in progress\")\n\t\treturn nil\n\t}\n\n\taccountEnabled, err := d.MonkeyCfg.AccountEnabled(account)\n\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"not terminating: could not determine if account is enabled\")\n\t}\n\n\tif !accountEnabled {\n\t\tlog.Printf(\"Not terminating: account=%s is not enabled in Chaos Monkey\", account)\n\t\treturn nil\n\t}\n\n\t// create an instance group from the command-line parameters\n\tgroup := grp.New(app, account, region, stack, cluster)\n\n\t// do the actual termination\n\treturn doTerminate(d, group)\n\n}\n\n// doTerminate does the actual termination\nfunc doTerminate(d deps.Deps, group grp.InstanceGroup) error {\n\tleashed, err := d.MonkeyCfg.Leashed()\n\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"not terminating: could not determine leashed status\")\n\t}\n\n\t/*\n\t\tDo not allow running unleashed in the test environment.\n\n\t\tThe prod deployment of chaos monkey is responsible for killing instances\n\t\tacross environments, including test. We want to ensure that Chaos Monkey\n\t\trunning in test cannot do harm.\n\t*/\n\tif d.Env.InTest() && !leashed {\n\t\treturn UnleashedInTestEnv{}\n\t}\n\n\tvar killer chaosmonkey.Terminator\n\n\tif leashed {\n\t\tkiller = leashedKiller{}\n\t} else {\n\t\tkiller = d.T\n\t}\n\n\t// get Chaos Monkey config info for this app\n\tappName := group.App()\n\tappCfg, err := d.ConfGetter.Get(appName)\n\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"not terminating: Could not retrieve config for app=%s\", appName)\n\t}\n\n\tif !appCfg.Enabled {\n\t\tlog.Printf(\"not terminating: enabled=false for app=%s\", appName)\n\t\treturn nil\n\t}\n\n\tif appCfg.Whitelist != nil {\n\t\tlog.Printf(\"not terminating: app=%s has a whitelist which is no longer supported\", appName)\n\t\treturn nil\n\t}\n\n\tinstance, ok := PickRandomInstance(group, *appCfg, d.Dep)\n\tif !ok {\n\t\tlog.Printf(\"No eligible instances in group, nothing to terminate: %+v\", group)\n\t\treturn nil\n\t}\n\n\tlog.Printf(\"Picked: %s\", instance)\n\n\tloc, err := d.MonkeyCfg.Location()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"not terminating: could not retrieve location\")\n\t}\n\n\ttrm := chaosmonkey.Termination{Instance: instance, Time: d.Cl.Now(), Leashed: leashed}\n\n\t//\n\t// Check that we don't violate min time between terminations\n\t//\n\terr = d.Checker.Check(trm, *appCfg, d.MonkeyCfg.EndHour(), loc)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"not terminating: check for min time between terminations failed\")\n\t}\n\n\t//\n\t// Record the termination with configured trackers\n\t//\n\tfor _, tracker := range d.Trackers {\n\t\terr = tracker.Track(trm)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"not terminating: recording termination event failed\")\n\t\t}\n\t}\n\n\t//\n\t// Actual instance termination happens here\n\t//\n\terr = killer.Execute(trm)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"termination failed\")\n\t}\n\n\treturn nil\n}\n\n// PickRandomInstance randomly selects an eligible instance from a group\nfunc PickRandomInstance(group grp.InstanceGroup, cfg chaosmonkey.AppConfig, dep deploy.Deployment) (chaosmonkey.Instance, bool) {\n\tinstances, err := eligible.Instances(group, cfg.Exceptions, dep)\n\tif err != nil {\n\t\tlog.Printf(\"WARNING: eligible.Instances failed for %s: %v\", group, err)\n\t\treturn nil, false\n\t}\n\tif len(instances) == 0 {\n\t\treturn nil, false\n\t}\n\n\tr := rand.New(rand.NewSource(time.Now().UnixNano()))\n\tindex := r.Intn(len(instances))\n\treturn instances[index], true\n}\n"
  },
  {
    "path": "term/term_ext_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage term_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/config/param\"\n\tD \"github.com/Netflix/chaosmonkey/v2/deploy\"\n\t\"github.com/Netflix/chaosmonkey/v2/mock\"\n\t\"github.com/Netflix/chaosmonkey/v2/term\"\n)\n\nfunc TestEnabledAccounts(t *testing.T) {\n\td := mock.Deps()\n\td.Dep = mock.NewDeployment(\n\t\tmap[string]D.AppMap{\n\t\t\t\"foo\": {\n\t\t\t\tD.AccountName(\"prod\"): {CloudProvider: \"aws\", Clusters: D.ClusterMap{D.ClusterName(\"foo\"): {D.RegionName(\"us-east-1\"): {D.ASGName(\"foo-v001\"): []D.InstanceID{\"i-00000000\"}}}}},\n\t\t\t\tD.AccountName(\"test\"): {CloudProvider: \"aws\", Clusters: D.ClusterMap{D.ClusterName(\"foo\"): {D.RegionName(\"us-east-1\"): {D.ASGName(\"foo-v001\"): []D.InstanceID{\"i-00000001\"}}}}},\n\t\t\t\tD.AccountName(\"mce\"):  {CloudProvider: \"aws\", Clusters: D.ClusterMap{D.ClusterName(\"foo\"): {D.RegionName(\"us-east-1\"): {D.ASGName(\"foo-v001\"): []D.InstanceID{\"i-00000002\"}}}}},\n\t\t\t},\n\t\t})\n\n\tapp := \"foo\"\n\tregion := \"us-east-1\"\n\tstack := \"\"\n\tcluster := \"\"\n\n\ttests := []struct {\n\t\tenabledAccounts []string\n\t\tkillAccount     string\n\t\twant            bool\n\t}{\n\t\t{[]string{\"prod\"}, \"prod\", true},\n\t\t{[]string{\"test\"}, \"test\", true},\n\t\t{[]string{\"mce\"}, \"mce\", true},\n\t\t{[]string{\"prod\"}, \"test\", false},\n\t\t{[]string{\"test\"}, \"prod\", false},\n\t\t{[]string{\"prod\"}, \"mce\", false},\n\t\t{[]string{\"prod\", \"test\"}, \"mce\", false},\n\t\t{[]string{\"mce\", \"prod\", \"test\"}, \"mce\", true},\n\t\t{[]string{\"prod\", \"mce\", \"test\"}, \"mce\", true},\n\t\t{[]string{\"prod\", \"test\", \"mce\"}, \"mce\", true},\n\t}\n\n\tfor _, test := range tests {\n\t\taccount := test.killAccount\n\n\t\t// Set up the mock config that will use the list of accounts we pass it\n\t\tcfg := config.Defaults()\n\t\tcfg.Set(param.Enabled, true)\n\t\tcfg.Set(param.Leashed, false)\n\t\tcfg.Set(param.Accounts, test.enabledAccounts)\n\n\t\td.MonkeyCfg = cfg\n\n\t\t// Set up the mock terminator that will track if a kill happened\n\t\t// create a new one each iteration so its state gets reset to zero\n\t\tmockT := new(mock.Terminator)\n\t\td.T = mockT\n\n\t\tif err := term.Terminate(d, app, account, region, stack, cluster); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif got, want := mockT.Ncalls == 1, test.want; got != want {\n\t\t\tt.Errorf(\"kill? (account=%s, enabledAccounts=%v, got %t, want %t, mockT.Ncalls=%d\", account, test.enabledAccounts, got, want, mockT.Ncalls)\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "term/terminate_test.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage term\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/clock\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/config/param\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n\t\"github.com/Netflix/chaosmonkey/v2/mock\"\n)\n\nfunc mockDeps() deps.Deps {\n\tmonkeyCfg := config.Defaults()\n\tmonkeyCfg.Set(param.Enabled, true)\n\tmonkeyCfg.Set(param.Leashed, false)\n\tmonkeyCfg.Set(param.Accounts, []string{\"prod\"})\n\trecorder := mock.Checker{Error: nil}\n\tconfGetter := mock.DefaultConfigGetter()\n\tcl := clock.New()\n\tdep := mock.Dep()\n\tttor := mock.Terminator{}\n\tou := mock.Outage{}\n\tenv := mock.Env{IsInTest: false}\n\treturn deps.Deps{MonkeyCfg: monkeyCfg, Checker: recorder, ConfGetter: confGetter, Cl: cl, Dep: dep, T: &ttor, Ou: ou, Env: env}\n}\n\n// TestTerminateKills ensure the terminator actually gets invoked\nfunc TestTerminateKills(t *testing.T) {\n\n\tdeps := mockDeps()\n\terr := Terminate(deps, \"foo\", \"prod\", \"us-east-1\", \"\", \"foo-prod\")\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tttor := deps.T.(*mock.Terminator)\n\tins := ttor.Instance\n\n\tif got, want := ttor.Ncalls, 1; got != want {\n\t\tt.Fatalf(\"Expected terminator to be called once, got ttor.Ncalls=%d\", ttor.Ncalls)\n\t}\n\n\tif got, want := ins.AppName(), \"foo\"; got != want {\n\t\tt.Errorf(\"Expected ins.AppName()=%s. want %s\", got, want)\n\t}\n\n\tif got, want := ins.AccountName(), \"prod\"; got != want {\n\t\tt.Errorf(\"Expected ins.AccountName()=%s. want %s\", got, want)\n\t}\n\n\tif got, want := ins.RegionName(), \"us-east-1\"; got != want {\n\t\tt.Errorf(\"Expected ins.RegionName()=%s. want %s\", got, want)\n\t}\n\n\tif got, want := ins.ClusterName(), \"foo-prod\"; got != want {\n\t\tt.Errorf(\"Expected ins.ClusterName()=%s. want %s\", got, want)\n\t}\n}\n\n// TestTerminateOnlyKillsInProd ensures we don't kill in non-prod accounts\n// This is temporary until we have full support for multiple accounts\nfunc TestTerminateOnlyKillsInProd(t *testing.T) {\n\tdeps := mockDeps()\n\n\terr := Terminate(deps, \"quux\", \"test\", \"us-east-1\", \"\", \"quux-test\")\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tttor := deps.T.(*mock.Terminator)\n\tif got, want := ttor.Ncalls, 0; got != want {\n\t\tt.Errorf(\"Expected terminator to not be called, got ttor.Ncalls=%d\", ttor.Ncalls)\n\t}\n\n}\n\nfunc TestTerminateDoesntKillIfRecorderFails(t *testing.T) {\n\tdeps := mockDeps()\n\tdeps.Checker = mock.Checker{Error: chaosmonkey.ErrViolatesMinTime{InstanceID: \"i-8703ada6\", KilledAt: time.Now().Add(-1 * time.Hour)}}\n\n\terr := Terminate(deps, \"foo\", \"prod\", \"us-east-1\", \"\", \"foo-prod\")\n\tif err == nil {\n\t\tt.Fatal(\"Expected Terminate to fail, it succeeded\")\n\t}\n\n\tttor := deps.T.(*mock.Terminator)\n\tif got, want := ttor.Ncalls, 0; got != want {\n\t\tt.Errorf(\"Expected terminator to not be called, got ttor.Ncalls=%d\", ttor.Ncalls)\n\t}\n}\n\n// TestTerminateDoesntKillInLeashedMode ensure terminator does not get invoked\n// if leashed is enabled\nfunc TestTerminateDoesntKillInLeashedMode(t *testing.T) {\n\n\tdeps := mockDeps()\n\tcfg := config.Defaults()\n\t// Setting leashed explicitly for code clarity, default is leashed so\n\t// this isn't strictly neededj\n\tcfg.Set(param.Leashed, true)\n\n\tdeps.MonkeyCfg = cfg\n\n\terr := Terminate(deps, \"foo\", \"prod\", \"us-east-1\", \"\", \"foo-prod\")\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tttor := deps.T.(*mock.Terminator)\n\tif got, want := ttor.Ncalls, 0; got != want {\n\t\tt.Errorf(\"Expected terminator to not be called, got ttor.Ncalls=%d\", ttor.Ncalls)\n\t}\n\n}\n\n// TestNeverTerminateInTestEnv checks that unleasshed terms are not allowed in\n// test\nfunc TestNeverTerminateUnleashedInTestEnv(t *testing.T) {\n\n\tdeps := mockDeps()\n\tdeps.Env = mock.Env{IsInTest: true}\n\n\terr := Terminate(deps, \"foo\", \"prod\", \"us-east-1\", \"\", \"foo-prod\")\n\n\tif _, ok := err.(UnleashedInTestEnv); !ok {\n\t\tt.Fatalf(\"Expected Terminate to return an error when running unleashed in test mode\")\n\t}\n\n\tttor := deps.T.(*mock.Terminator)\n\tif got, want := ttor.Ncalls, 0; got != want {\n\t\tt.Errorf(\"Expected terminator to be called once, got ttor.Ncalls=%d\", ttor.Ncalls)\n\t}\n\n}\n\nfunc TestDoesNotTerminateIfTrackerFails(t *testing.T) {\n\tdeps := mockDeps()\n\n\t// We pass two trackers, the first one succeeds, the second returns an error\n\tdeps.Trackers = []chaosmonkey.Tracker{\n\t\tmock.Tracker{},\n\t\tmock.Tracker{Error: errors.New(\"something went wrong\")}}\n\n\terr := Terminate(deps, \"foo\", \"prod\", \"us-east-1\", \"\", \"foo-prod\")\n\tif err == nil {\n\t\tt.Fatal(\"Tracker failed but Terminate did not return an error\")\n\t}\n\n\tttor := deps.T.(*mock.Terminator)\n\tif got, want := ttor.Ncalls, 0; got != want {\n\t\tt.Errorf(\"Expected terminator to not be called, got ttor.Ncalls=%d\", ttor.Ncalls)\n\t}\n\n}\n\nfunc TestDoesNotTerminateIfAppIsDisabled(t *testing.T) {\n\tdeps := mockDeps()\n\n\t// Disable app\n\tdeps.ConfGetter = mock.NewConfigGetter(chaosmonkey.AppConfig{\n\t\tEnabled:                        false,\n\t\tRegionsAreIndependent:          true,\n\t\tMeanTimeBetweenKillsInWorkDays: 5,\n\t\tMinTimeBetweenKillsInWorkDays:  1,\n\t\tGrouping:                       chaosmonkey.Cluster,\n\t\tExceptions:                     nil,\n\t})\n\n\terr := Terminate(deps, \"foo\", \"prod\", \"us-east-1\", \"\", \"foo-prod\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tttor := deps.T.(*mock.Terminator)\n\tif got, want := ttor.Ncalls, 0; got != want {\n\t\tt.Errorf(\"Expected terminator to not be called, got ttor.Ncalls=%d\", ttor.Ncalls)\n\t}\n}\n"
  },
  {
    "path": "term/terminator.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage term\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/Netflix/chaosmonkey/v2\"\n)\n\n// fake is a fake implementation of a terminator that just prints termination info but does nothing\ntype fake struct{}\n\n// Fake returns a \"fake\" terminator that just outputs a message upon instance termination\nfunc Fake() chaosmonkey.Terminator {\n\treturn fake{}\n}\n\n// Kill implements Terminator.kill, pretends to terminate an instance\nfunc (t fake) Execute(trm chaosmonkey.Termination) error {\n\tins := trm.Instance\n\tfmt.Printf(\"fakeTerminator fake-terminating: account=%s region=%s id=%s\\n\", ins.AccountName(), ins.RegionName(), ins.ID())\n\treturn nil\n}\n"
  },
  {
    "path": "tracker/tracker.go",
    "content": "// Copyright 2016 Netflix, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package tracker provides an entry point for instantiating Trackers\npackage tracker\n\nimport (\n\t\"github.com/Netflix/chaosmonkey/v2\"\n\t\"github.com/Netflix/chaosmonkey/v2/config\"\n\t\"github.com/Netflix/chaosmonkey/v2/deps\"\n\t\"github.com/pkg/errors\"\n)\n\nfunc init() {\n\tdeps.GetTrackers = getTrackers\n}\n\n// getTrackers returns a list of trackers specified in the configuration\nfunc getTrackers(cfg *config.Monkey) ([]chaosmonkey.Tracker, error) {\n\tvar result []chaosmonkey.Tracker\n\n\tkinds, err := cfg.Trackers()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, kind := range kinds {\n\t\ttr, err := getTracker(kind, cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresult = append(result, tr)\n\t}\n\treturn result, nil\n}\n\n// getTracker returns a tracker by name\n// No trackers have been implemented yet\nfunc getTracker(kind string, cfg *config.Monkey) (chaosmonkey.Tracker, error) {\n\tswitch kind {\n\t// As trackers are contributed to the open source project, they should\n\t// be instantiated here\n\tdefault:\n\t\treturn nil, errors.Errorf(\"unsupported tracker: %s\", kind)\n\t}\n}\n"
  },
  {
    "path": "update-docs.sh",
    "content": "#!/bin/bash\nset -e\necho \"DEPLOY_DOCS=$DEPLOY_DOCS\"\nif [[ $DEPLOY_DOCS != true ]]; then\n    echo \"Not building docs\"\n    exit 0\nfi\n\necho \"Building docs\"\n\n# Install mkdocs\nvirtualenv venv\nvenv/bin/pip install mkdocs\n\n# Decrypt and load the ssh key\nopenssl aes-256-cbc -K $encrypted_5704967818cd_key -iv $encrypted_5704967818cd_iv -in docKey.enc -out docKey -d\nchmod 0600 docKey\neval `ssh-agent -s`\nssh-add docKey\n\n\n# Push up to gh-pages\n# --force is required otherwise it will fail to push up\nvenv/bin/mkdocs gh-deploy --remote-name git@github.com:Netflix/chaosmonkey.git --force\n"
  }
]