pax_global_header00006660000000000000000000000064146375661110014524gustar00rootroot0000000000000052 comment=8d235069226e9b48214fcb8d0fb2250c67828bf8 puppetlabs-trapperkeeper-d1f1135/000077500000000000000000000000001463756611100171005ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/.clj-kondo/000077500000000000000000000000001463756611100210365ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/.clj-kondo/config.edn000066400000000000000000000040551463756611100227770ustar00rootroot00000000000000{:linters {:unresolved-symbol {:level :warning :exclude [(puppetlabs.trapperkeeper.services/service) (puppetlabs.trapperkeeper.core/defservice) (puppetlabs.trapperkeeper.core/service) (puppetlabs.trapperkeeper.services/defservice) (clojure.test/is [thrown+? thrown+-with-msg? logged?]) (puppetlabs.trapperkeeper.testutils.bootstrap/with-app-with-cli-data) (puppetlabs.trapperkeeper.testutils.bootstrap/with-app-with-config) (puppetlabs.trapperkeeper.testutils.bootstrap/with-app-with-empty-config) (puppetlabs.trapperkeeper.testutils.bootstrap/with-app-with-cli-args) (puppetlabs.trapperkeeper.testutils.logging/with-started) (puppetlabs.trapperkeeper.testutils.logging/with-logger-event-maps) (puppetlabs.trapperkeeper.testutils.logging/with-logged-event-maps)]} :invalid-arity {:skip-args [puppetlabs.trapperkeeper.services/service puppetlabs.trapperkeeper.services/defservice puppetlabs.trapperkeeper.core/defservice puppetlabs.trapperkeeper.core/service]} :refer-all {:level :off} :inline-def {:level :off} :deprecated-var {:level :off}} :lint-as {puppetlabs.trapperkeeper.core/defservice clojure.core/def puppetlabs.trapperkeeper.services/defservice clojure.core/def slingshot.slingshot/try+ clojure.core/try puppetlabs.kitchensink.core/while-let clojure.core/let}}puppetlabs-trapperkeeper-d1f1135/.github/000077500000000000000000000000001463756611100204405ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/.github/workflows/000077500000000000000000000000001463756611100224755ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/.github/workflows/clojure-linting.yaml000066400000000000000000000015441463756611100264720ustar00rootroot00000000000000name: Clojure Linting on: pull_request: types: [opened, reopened, edited, synchronize] paths: ['src/**','test/**','.clj-kondo/config.edn','project.clj','.github/**'] jobs: clojure-linting: name: Clojure Linting runs-on: ubuntu-latest steps: - name: setup java uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: checkout repo uses: actions/checkout@v4 - name: install clj-kondo (this is quite fast) run: | curl -sLO https://raw.githubusercontent.com/clj-kondo/clj-kondo/master/script/install-clj-kondo chmod +x install-clj-kondo ./install-clj-kondo --dir . - name: kondo lint run: ./clj-kondo --lint src test - name: eastwood lint run: | java -version lein eastwoodpuppetlabs-trapperkeeper-d1f1135/.github/workflows/lein-test.yaml000066400000000000000000000012271463756611100252670ustar00rootroot00000000000000name: PR Testing on: workflow_dispatch: pull_request: types: [opened, reopened, edited, synchronize] paths: ['src/**','test/**','project.clj'] jobs: pr-testing: name: PR Testing strategy: fail-fast: false matrix: version: ['8', '11', '17'] runs-on: ubuntu-latest steps: - name: checkout repo uses: actions/checkout@v3 with: submodules: recursive - name: setup java uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: ${{ matrix.version }} - name: clojure tests run: lein test timeout-minutes: 30puppetlabs-trapperkeeper-d1f1135/.github/workflows/mend.yaml000066400000000000000000000035471463756611100243150ustar00rootroot00000000000000name: mend_scan on: schedule: # run every day at 4:00am - cron: 0 4 * * * workflow_dispatch: push: branches: - main jobs: build: runs-on: ubuntu-latest steps: - name: connect_twingate uses: twingate/github-action@v1 with: service-key: ${{ secrets.TWINGATE_PUBLIC_REPO_KEY }} - name: checkout repo content uses: actions/checkout@v4 # checkout the repository content to github runner. with: fetch-depth: 1 # install java which is required for mend and clojure - name: setup java uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 # install clojure tools - name: Install Clojure tools uses: DeLaGuardo/setup-clojure@12.5 with: # Install just one or all simultaneously # The value must indicate a particular version of the tool, or use 'latest' # to always provision the latest version cli: latest # Clojure CLI based on tools.deps lein: latest # Leiningen boot: latest # Boot.clj bb: latest # Babashka clj-kondo: latest # Clj-kondo cljstyle: latest # cljstyle zprint: latest # zprint # run lein gen - name: create pom.xml run: lein pom # download mend - name: download_mend run: curl -o wss-unified-agent.jar https://unified-agent.s3.amazonaws.com/wss-unified-agent.jar - name: run mend run: env WS_INCLUDES=pom.xml java -jar wss-unified-agent.jar env: WS_APIKEY: ${{ secrets.MEND_API_KEY }} WS_WSS_URL: https://saas-eu.whitesourcesoftware.com/agent WS_USERKEY: ${{ secrets.MEND_TOKEN }} WS_PRODUCTNAME: Puppet Enterprise WS_PROJECTNAME: ${{ github.event.repository.name }} puppetlabs-trapperkeeper-d1f1135/.gitignore000066400000000000000000000002701463756611100210670ustar00rootroot00000000000000 # Emacs *# *~ .#* .lein-failures .lein-repl-history target/ checkouts/ pom.xml .nrepl-port /resources/locales.clj /resources/puppetlabs/trapperkeeper/*.class /dev-resources/i18n/bin puppetlabs-trapperkeeper-d1f1135/.travis.yml000066400000000000000000000015521463756611100212140ustar00rootroot00000000000000language: clojure lein: 2.9.1 jobs: include: - jdk: openjdk8 name: lein test (openjdk8) - jdk: openjdk11 name: lein test (openjdk11) - jdk: openjdk8 name: external tests (openjdk8) script: lein uberjar && ext/test/run-all - jdk: openjdk11 name: external tests (openjdk11) script: lein uberjar && ext/test/run-all - name: external tests (openjdk11) # Apparently travis' lein support is broken right now language: java os: osx osx_image: xcode10.3 script: | # prep in standalone script so things like set -x don't affect travis ext/travisci/prep-macos \ && export PATH="$(pwd)/ext/travisci/bin:$PATH" \ && lein uberjar \ && ext/test/run-all notifications: email: false cache: directories: - $HOME/.m2 - $HOME/Library/Caches/Homebrew puppetlabs-trapperkeeper-d1f1135/CHANGELOG.md000066400000000000000000000336621463756611100207230ustar00rootroot00000000000000## 4.0.2 * update `logged?` to not emit an incorrect message when there are no matches, and clean up the output from multiple unexpected matches. ## 4.0.1 * adds a new arity to `logged?` that removes the restriction that only one log line must match the pattern, adds printing to the function and repo documentation to make users aware of this single line match restriction ## 4.0.0 This is a major release with breaking changes. * remove support for yaml configuration files * add clj-kondo linting and fix issues found * add eastwood linting and fix issues found ## 3.3.1 This is a maintenance release * Unpin logback and logback dependencies versions, bump clj-parent to defer to its versioning ## 3.3.0 This is a potentially breaking dependency version update release * Upgrades logback version to 1.3.5 from 1.2.9, which is now in maintenance mode * Adds slf4j-api dependency and pins other relevant dependencies to 2.0.6 ## 3.2.1 This is a maintenance release * Avoid crashing when trying to load bootstrap.cfg in some cases. See also [PDB-5215](https://tickets.puppetlabs.com/browse/PDB-5215) and [CLJ-2431](https://clojure.atlassian.net/browse/CLJ-2431)). ## 3.2.0 This is a minor feature release * Backward compatible changes to the signature of `puppetlabs.trapperkeeper.internal/shutdown!` function. Returns collection of exceptions caught during execution of shutdown sequence instead of nil. * Extend `stop` method of `puppetlabs.trapperkeeper.app/TrapperkeeperApp` protocol with an argument `throw?` to handle cases where exceptions in shutdown sequence should be rethrown. * Change default behavior of `puppetlabs.trapperkeeper.testutils.bootstrap` helper macroses to throw exception when shutdown finished abruptly. ## 3.1.1 This is a maintenance release * Updates to current clj-parent ## 3.1.0 This is a minor feature release * [PDB-4636](https://github.com/puppetlabs/trapperkeeper/pull/287) - support custom exit status/messages ## 3.0.0 This is a maintenance release * Updates to current clj-parent to clean up project.clj and update dependencies * Tests changes for readability and compatibility with Java11 ## 2.0.1 This is a maintenance release * Ensures that all errors are correctly thrown, notably errors about bad config schemas. ## 2.0.0 This is a maintenance release * [ORCH-2282](https://tickets.puppetlabs.com/browse/ORCH-2282) - Updates to current clj-parent to support using nrepl/nrepl * Updates required for using nrepl/nrepl; mainline development for nrepl moved from org.clojure/tools.nrepl as of the 0.3.x series (last on this line was 0.2.13) * Updating to this version of trapperkeeper requires lein >=2.9.0 (:min-lein-version updated) * Drops support for JDK7 ## 1.5.6 This is a maintenance release * [TK-466](https://tickets.puppetlabs.com/browse/TK-466) - Log SIGHUP events at INFO level ## 1.5.5 This is a maintenance release * Fix log message accidentally converted to a warning ## 1.5.4 This is a maintenance release. * Fix adding to classpath under Java 9 ## 1.5.3 This is a maintenance release. * [TK-411](https://tickets.puppetlabs.com/browse/TK-411) - Externalize strings for i18n * [TK-439](https://tickets.puppetlabs.com/browse/TK-439) - Handle exceptions having no type key in main * Fix symbol redef warnings under Clojure 1.9 * Improved lifecycle debug logging ## 1.5.2 This is a maintenance release. * [SERVER-1494](https://tickets.puppetlabs.com/browse/SERVER-1494) - use `lein-parent` plugin to inherit dependency versions from parent project. ## 1.5.1 This is a minor feature release * [TK-405](https://tickets.puppetlabs.com/browse/TK-405) - Add support for specifying the restart file option via a command line argument ## 1.5.0 This is a feature/bugfix/maintenance release * [TK-345](https://tickets.puppetlabs.com/browse/TK-345) - Add support for optional restart file which, if specified, will contain an integer that increments when a TK app has successfully started all of its services * [TK-382](https://tickets.puppetlabs.com/browse/TK-382) - Fix bug where optional dependencies could not be specified for a service without a protocol * [TK-397](https://tickets.puppetlabs.com/browse/TK-397) - Update to logback 1.1.7 ## 1.4.1 This is a bugfix release. It fixes a single issue * [TK-375](https://tickets.puppetlabs.com/browse/TK-375) - Regression in 1.4.0 when loading bootstrap.cfg from resources/classpath ## 1.4.0 This is feature/bugfix release. It is a re-release of 1.3.2 * [TK-347](https://tickets.puppetlabs.com/browse/TK-347) - Support directories and paths in TK's "bootstrap-config" CLI argument * [TK-211](https://tickets.puppetlabs.com/browse/TK-211) - Trapperkeeper doesn't error if two services implementing the same protocol are started * [TK-349](https://tickets.puppetlabs.com/browse/TK-349) - TK should not fail during startup if an unrecognized service is found in bootstrap config * [TK-351](https://tickets.puppetlabs.com/browse/TK-351) - Ensure all bootstrap related errors log what file they come from ## 1.3.2 This version was released by mistake, it was intended to be 1.4.0 ## 1.3.1 This is a bugfix / maintenance / minor feature release * [TK-319](https://tickets.puppetlabs.com/browse/TK-319) - fix a bug where optional dependencies could not be used without a service protocol * [TK-325](https://tickets.puppetlabs.com/browse/TK-325) - move documentation into repo, instead of storing it on the github wiki * [HC-51](https://tickets.puppetlabs.com/browse/HC-51) - update to newer version of clj typesafe / hocon wrapper, fixing bug that prevented variable interpolation from working properly in hocon config files * New `bootstrap-services-with-config` testutils macro * [TK-342](https://tickets.puppetlabs.com/browse/TK-342) - new logging testutils macros, e.g. `with-logged-event-maps`. * [TK-326](https://tickets.puppetlabs.com/browse/TK-326), [TK-330](https://tickets.puppetlabs.com/browse/TK-330), [TK-331](https://tickets.puppetlabs.com/browse/TK-331) - various minor improvements to HUP support to eliminate some bugs/annoyances that were possible in pathological situations ## 1.3.0 This is a feature release. * [TK-202](https://tickets.puppetlabs.com/browse/TK-202) - adds support for restarting a TK app via HUP signal, w/o shutting down entire JVM process * [TK-315](https://tickets.puppetlabs.com/browse/TK-315) - update raynes.fs dependency to 1.4.6, to minimize dependency conflicts for consumers * RELEASE NOTE: adds a dependency on core.async * RELEASE NOTE: minor changes to internal `app-context` API; all service contexts are now stored under a key called `:service-contexts`. This shouldn't affect any consuming code unless you were digging into the internal `app-context` API for really low-level tests or similar. ## 1.2.0 This is a minor feature release. * [TK-299](https://tickets.puppetlabs.com/browse/TK-299) - support optional dependencies, which allow services to take advantage of other services if they're included in the bootstrap and gracefully handled when they are not included. See the [docs](https://github.com/puppetlabs/trapperkeeper/wiki/Defining-Services#optional-services) for more detail. * Use newer version of schema library and make use of more schemas. ## 1.1.3 This is a bugfix release. * [TK-311](https://tickets.puppetlabs.com/browse/TK-311) - fix a minor bug in the new logging testutils, where the log appenders weren't implementing the `isStarted` method. ## 1.1.2 This is a bugfix / minor feature release. * Various, significant improvements to logging testutils, courtesy of Rob Browning. * [TK-291](https://tickets.puppetlabs.com/browse/TK-291) - `(is (logged?` test assertion now captures log messages that were logged by other (non-Clojure) threads. * `logs-matching` now has an additional signature that accepts a log level * Improvements to error handling when an error occurs in TK's `main` function ## 1.1.1 This is a maintenance / minor feature release. * [TK-197](https://tickets.puppetlabs.com/browse/TK-197) - update prismatic dependencies to latest versions. * Add support for yaml config files * [TK-131](https://tickets.puppetlabs.com/browse/TK131) Relax preconditions on logging configuration ## 1.1.0 This is a minor feature release. * Add support for logback's `EvaluatorFilter`, which allows users to configure the logging to filter log messages based on regular expression patterns. ## 1.0.1 * Fix an issue wherein nothing would be logged to the console when the --debug flag was set ## 1.0.0 * Promoting previous release to 1.0.0, so that we can begin to be more deliberate about adhering to semver from here on out. ## 0.5.2 This is a minor feature and bugfix release. * Call the `service-symbol` function in lifecycle error messages to make it easier to determine which service caused the error * Fix an IllegalArgumentException that would occur when catching a slingshot exception in the TK `main` function. * Allow multiple comma-separated config files and directories to be specified in the --config CLI argument. ## 0.5.1 This is a bugfix release. * Fix a bug that prevented `defservice` from working with protocols that were defined in a different namespace. ## 0.5.0 This is a feature release with a minor breaking API change. * The breaking API change affects the functions defined in the `puppetlabs.trapperkeeper.services/Service` protocol - namely, `service-context`. References to these functions are no longer automatically in scope inside a `service` or `defservice` definition as they were previously (via macro magic), and they must be `require`d like any other function - `(require '[puppetlabs.trapperkeeper.services :refer [service-context]])`. * Changed schema version to support the Bool type * Improve implementation of the `service` macro * Formalize public function for loading config ## 0.4.3 This is a minor feature release. * Moved documentation to github wiki * Get rid of requirement for `--config` command-line argument * Add new `service-symbol` and `get-services` functions to protocols * Update dependencies ## 0.4.2 This is a minor feature release. * Add a new configuration setting `middlewares` to the nREPL service, to allow registration of nREPL middleware (e.g. for compatibility with LightTable). (Thanks to `exi` for this contribution!) ## 0.4.1 This is a maintenance/bugfix release. * Fix a minor bug in testutils/logging where we inadvertently changed the return value of log statements. * Add an explicit call to `shutdown-agents` on trapperkeeper exit, to prevent the JVM from hanging for 60 seconds on shutdown (if any services were using `future`). ## 0.4.0 This release includes improved error handling and logic for shutting down Trapperkeeper applications. * Improved handling of errors during a service's `init` or `start` functions: * All services' `stop` functions are now called, even when an error is thrown by any service's `init` or `start` function. This means that `stop` implementations must now be resilient to invocation even when `init` or `start` has not executed. * Updated `boot-services-with-cli-data`, `boot-services-with-config`, and `boot-with-cli-data` to return the `TrapperkeeperApp` instance rather than propagating the `Throwable`. * Updated example "Reloaded" pattern usage to use the new `check-for-errors!` function on the `TrapperkeeperApp` instance to detect any errors that may have occurred while services were being bootstrapped. ## 0.3.12 This is a maintenance release. * Upgrade fs dependency to 1.4.5 to standardize across projects ## 0.3.11 This is a maintenance/bugfix release. * Fix minor bug in how nrepl service loads its configuration * Add CONTRIBUTING.md file * Fix a few misleading things in the README (dan@simple.com) ## 0.3.10 This is a maintenance release. * Update version number of kitchensink dependency to 0.6.0, to get rid of transitive dependencies on SSL libraries. ## 0.3.9 This is a maintenance release. * Update version number of logback dependency from 1.0.13 to 1.1.1, to resolve a bug in logback that was affecting our jetty9 web server. ## 0.3.8 This is a bugfix and maintenance release. * Improve logging of exceptions that occur during bootstrapping. ## 0.3.7 This is a bugfix and maintenance release. * Log exceptions that occur during bootstrapping. ## 0.3.6 This is a bugfix and maintenance release. * Move typesafe config code to an external library - https://github.com/puppetlabs/clj-typesafe-config * Improve error handling and logging in `shutdown-on-error`. ## 0.3.5 * Improved error handling in the `service`/`defservice` macros. * Improved error handling in the shutdown logic, particularly when using `shutdown-on-error`. * Fix a bug that prevented `service-id` from being called from a service's `init` function. * Minor documentation fixes and improvements. ## 0.3.4 * Add new macros in `testutils/bootstrap` namespace, to make it easier to write tests for services * Add support for .edn, .conf, .json, .properties config files in addition to .ini ## 0.3.3 * Fix a bug in how we were handling command-line arguments in the case where the user does not pass any * Add a new function `get-service` to the `Service` protocol, which allows service authors to get a reference to the protocol instance of a service if they prefer that to the prismatic-style function injections ## 0.3.2 * Bugfix release * Use prismatic schema validation during tests to ensure we are complying with our advertised schemas * Fix bug where we were not including lifecycle functions in the schema * Fix bug in error handling of prismatic exceptions * Upgrade to version 0.2.1 of prismatic schema, which includes a fix for some thing related to aot. ## 0.3.0 * Changes to `defservice` API so that it supports service lifecycles more explicitly, and now uses clojure protocols as the means for specifying functions provided by a service. * Upgrade to 0.5.1 of kitchensink, which includes a significant performance improvement for applications that are accepting HTTPS connections puppetlabs-trapperkeeper-d1f1135/CODEOWNERS000066400000000000000000000000271463756611100204720ustar00rootroot00000000000000* @puppetlabs/dumpling puppetlabs-trapperkeeper-d1f1135/CONTRIBUTING.md000066400000000000000000000010241463756611100213260ustar00rootroot00000000000000# How to contribute Third-party patches are essential for keeping Puppet Labs open-source projects great. We want to keep it as easy as possible to contribute changes that allow you to get the most out of our projects. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. For more info, see our canonical guide to contributing: [https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md](https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md) puppetlabs-trapperkeeper-d1f1135/LICENSE000066400000000000000000000260751463756611100201170ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. puppetlabs-trapperkeeper-d1f1135/Makefile000066400000000000000000000000431463756611100205350ustar00rootroot00000000000000include dev-resources/Makefile.i18npuppetlabs-trapperkeeper-d1f1135/README.md000066400000000000000000000070601463756611100203620ustar00rootroot00000000000000Trapperkeeper logo # Trapperkeeper [![Build Status](https://travis-ci.org/puppetlabs/trapperkeeper.png?branch=master)](https://travis-ci.org/puppetlabs/trapperkeeper) Trapperkeeper is a Clojure framework for hosting long-running applications and services. You can think of it as a sort of "binder" for Ring applications and other modular bits of Clojure code. ## Installation Add the following dependency to your `project.clj` file: [![Clojars Project](http://clojars.org/puppetlabs/trapperkeeper/latest-version.svg)](http://clojars.org/puppetlabs/trapperkeeper) ## Community * Bug reports and feature requests: you can submit a Github issue, but we use [JIRA](https://tickets.puppetlabs.com/browse/TK) as our main issue tracker. * freenode: #trapperkeeper * [![Join the chat at https://gitter.im/puppetlabs/trapperkeeper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/puppetlabs/trapperkeeper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Documentation You can find a quick-start, example code, and lots and lots of documentation in our: * [Documentation](documentation/Index.md) ## Lein Template A Leiningen template is available that shows a suggested project structure: lein new trapperkeeper my.namespace/myproject Once you've created a project from the template, you can run it via the lein alias: lein tk Note that the template is not intended to suggest a specific namespace organization; it's just intended to show you how to write a service, a web service, and tests for each. ## Related Projects Here are some additional projects that provide Trapperkeeper services, and other related functionality: * [trapperkeeper-webserver-jetty9](https://github.com/puppetlabs/trapperkeeper-webserver-jetty9): a Jetty9-based webserver for use with TK applications * [trapperkeeper-rpc](https://github.com/puppetlabs/trapperkeeper-rpc): a TK service that allows you to easily build a way to call remote TK services over RPC * [trapperkeeper-metrics](https://github.com/puppetlabs/trapperkeeper-metrics): a TK service that manages the life cycle of a [MetricRegistry](https://github.com/dropwizard/metrics), so that all of your TK services can register metrics with a common configuration syntax. * [trapperkeeper-comidi-metrics](https://github.com/puppetlabs/trapperkeeper-comidi-metrics): a TK utility library that provides middleware to automatically generate metrics for all requests to each of your bidi/comidi HTTP routes. * [trapperkeeper-status](https://github.com/puppetlabs/trapperkeeper-status): a TK service that provides a mechanism for registering status callbacks for all of your other TK services, and web API for requesting status information about the entire TK system. * [trapperkeeper-scheduler](https://github.com/puppetlabs/trapperkeeper-scheduler): a TK service that provides an API for scheduling periodic background tasks ## License Copyright © 2013 Puppet Labs Distributed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) ## Support Please log tickets and issues at our [JIRA tracker](https://tickets.puppetlabs.com/browse/TK). There is also a #trapperkeeper channel on Freenode as well as [![Join the chat at https://gitter.im/puppetlabs/trapperkeeper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/puppetlabs/trapperkeeper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge). puppetlabs-trapperkeeper-d1f1135/dev-resources/000077500000000000000000000000001463756611100216665ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/Makefile.i18n000066400000000000000000000141051463756611100241050ustar00rootroot00000000000000# -*- Makefile -*- # This file was generated by the i18n leiningen plugin # Do not edit this file; it will be overwritten the next time you run # lein i18n init # # The name of the package into which the translations bundle will be placed BUNDLE=puppetlabs.trapperkeeper # The name of the POT file into which the gettext code strings (msgid) will be placed POT_NAME=trapperkeeper.pot # The list of names of packages covered by the translation bundle; # by default it contains a single package - the same where the translations # bundle itself is placed - but this can be overridden - preferably in # the top level Makefile PACKAGES?=$(BUNDLE) LOCALES=$(basename $(notdir $(wildcard locales/*.po))) BUNDLE_DIR=$(subst .,/,$(BUNDLE)) BUNDLE_FILES=$(patsubst %,resources/$(BUNDLE_DIR)/Messages_%.class,$(LOCALES)) FIND_SOURCES=find src -name \*.clj # xgettext before 0.19 does not understand --add-location=file. Even CentOS # 7 ships with an older gettext. We will therefore generate full location # info on those systems, and only file names where xgettext supports it LOC_OPT=$(shell xgettext --add-location=file -f - /dev/null 2>&1 && echo --add-location=file || echo --add-location) LOCALES_CLJ=resources/locales.clj define LOCALES_CLJ_CONTENTS { :locales #{$(patsubst %,"%",$(LOCALES))} :packages [$(patsubst %,"%",$(PACKAGES))] :bundle $(patsubst %,"%",$(BUNDLE).Messages) } endef export LOCALES_CLJ_CONTENTS i18n: msgfmt # Update locales/.pot update-pot: locales/$(POT_NAME) locales/$(POT_NAME): $(shell $(FIND_SOURCES)) | locales @tmp=$$(mktemp $@.tmp.XXXX); \ $(FIND_SOURCES) \ | xgettext --from-code=UTF-8 --language=lisp \ --copyright-holder='Puppet ' \ --package-name="$(BUNDLE)" \ --package-version="$(BUNDLE_VERSION)" \ --msgid-bugs-address="docs@puppet.com" \ -k \ -kmark:1 -ki18n/mark:1 \ -ktrs:1 -ki18n/trs:1 \ -ktru:1 -ki18n/tru:1 \ -ktrun:1,2 -ki18n/trun:1,2 \ -ktrsn:1,2 -ki18n/trsn:1,2 \ $(LOC_OPT) \ --add-comments --sort-by-file \ -o $$tmp -f -; \ sed -i.bak -e 's/charset=CHARSET/charset=UTF-8/' $$tmp; \ sed -i.bak -e 's/POT-Creation-Date: [^\\]*/POT-Creation-Date: /' $$tmp; \ rm -f $$tmp.bak; \ if ! diff -q -I POT-Creation-Date $$tmp $@ >/dev/null 2>&1; then \ mv $$tmp $@; \ else \ rm $$tmp; touch $@; \ fi # Run msgfmt over all .po files to generate Java resource bundles # and create the locales.clj file msgfmt: $(BUNDLE_FILES) $(LOCALES_CLJ) clean-orphaned-bundles # Force rebuild of locales.clj if its contents is not the the desired one. The # shell echo is used to add a trailing newline to match the one from `cat` ifneq ($(shell cat $(LOCALES_CLJ) 2> /dev/null),$(shell echo '$(LOCALES_CLJ_CONTENTS)')) .PHONY: $(LOCALES_CLJ) endif $(LOCALES_CLJ): | resources @echo "Writing $@" @echo "$$LOCALES_CLJ_CONTENTS" > $@ # Remove every resource bundle that wasn't generated from a PO file. # We do this because we used to generate the english bundle directly from the POT. .PHONY: clean-orphaned-bundles clean-orphaned-bundles: @for bundle in resources/$(BUNDLE_DIR)/Messages_*.class; do \ locale=$$(basename "$$bundle" | sed -E -e 's/\$$?1?\.class$$/_class/' | cut -d '_' -f 2;); \ if [ ! -f "locales/$$locale.po" ]; then \ rm "$$bundle"; \ fi \ done resources/$(BUNDLE_DIR)/Messages_%.class: locales/%.po | resources msgfmt --java2 -d resources -r $(BUNDLE).Messages -l $(*F) $< # Use this to initialize translations. Updating the PO files is done # automatically through a CI job that utilizes the scripts in the project's # `bin` file, which themselves come from the `clj-i18n` project. locales/%.po: | locales @if [ ! -f $@ ]; then \ touch $@ && msginit --no-translator -l $(*F) -o $@ -i locales/$(POT_NAME); \ fi resources locales: @mkdir $@ help: $(info $(HELP)) @echo .PHONY: help define HELP This Makefile assists in handling i18n related tasks during development. Files that need to be checked into source control are put into the locales/ directory. They are locales/$(POT_NAME) - the POT file generated by 'make update-pot' locales/$$LANG.po - the translations for $$LANG Only the $$LANG.po files should be edited manually; this is usually done by translators. You can use the following targets: i18n: refresh all the files in locales/ and recompile resources update-pot: extract strings and update locales/$(POT_NAME) locales/LANG.po: create translations for LANG msgfmt: compile the translations into Java classes; this step is needed to make translations available to the Clojure code and produces Java class files in resources/ endef # @todo lutter 2015-04-20: for projects that use libraries with their own # translation, we need to combine all their translations into one big po # file and then run msgfmt over that so that we only have to deal with one # resource bundle puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/000077500000000000000000000000001463756611100245615ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/classpath/000077500000000000000000000000001463756611100265435ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/classpath/bootstrap.cfg000066400000000000000000000002471463756611100312440ustar00rootroot00000000000000puppetlabs.trapperkeeper.examples.bootstrapping.test-services/classpath-test-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/000077500000000000000000000000001463756611100253305ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/bootstrap.cfg000066400000000000000000000002411463756611100300230ustar00rootroot00000000000000puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/bootstrap_with_comments.cfg000066400000000000000000000004701463756611100327670ustar00rootroot00000000000000# commented out line puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service # comment ; another commented out line ;puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service ; comment puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/duplicate_entries.cfg000066400000000000000000000006321463756611100315150ustar00rootroot00000000000000puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/duplicate_services/000077500000000000000000000000001463756611100312055ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/duplicate_services/duplicates.cfg000066400000000000000000000007161463756611100340270ustar00rootroot00000000000000# cli and foo implement the same service protocol puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service # test-service-two and test-service-two-duplicate implement the same service protocol puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two-duplicate puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/duplicate_services/split_one.cfg000066400000000000000000000004601463756611100336620ustar00rootroot00000000000000# cli and foo implement the same service protocol puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service # test-service-two and test-service-two-duplicate implement the same service protocol puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two-duplicate puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/duplicate_services/split_two.cfg000066400000000000000000000004461463756611100337160ustar00rootroot00000000000000# cli and foo implement the same service protocol puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service # test-service-two and test-service-two-duplicate implement the same service protocol puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/empty_bootstrap.cfg000066400000000000000000000000001463756611100312320ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/fake_namespace_bootstrap.cfg000066400000000000000000000003031463756611100330240ustar00rootroot00000000000000puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service non-existent-service/test-service puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/invalid_entry_bootstrap.cfg000066400000000000000000000001511463756611100327520ustar00rootroot00000000000000puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service This is not a legit line. puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/invalid_service_graph_bootstrap.cfg000066400000000000000000000001341463756611100344330ustar00rootroot00000000000000puppetlabs.trapperkeeper.examples.bootstrapping.test-services/invalid-service-graph-service puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/missing_definition_bootstrap.cfg000066400000000000000000000003641463756611100337720ustar00rootroot00000000000000puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/non-existent-service puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/path with spaces/000077500000000000000000000000001463756611100304575ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/path with spaces/bootstrap.cfg000066400000000000000000000002411463756611100331520ustar00rootroot00000000000000puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/000077500000000000000000000000001463756611100307435ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/both/000077500000000000000000000000001463756611100316775ustar00rootroot00000000000000bootstrap_one.cfg000066400000000000000000000002411463756611100351540ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/bothpuppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service bootstrap_two.cfg000066400000000000000000000002401463756611100352030ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/bothpuppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-three puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/empty/000077500000000000000000000000001463756611100321015ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/empty/empty1.cfg000066400000000000000000000000001463756611100337670ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/empty/empty2.cfg000066400000000000000000000000341463756611100337770ustar00rootroot00000000000000# any entries here? # nope puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/one/000077500000000000000000000000001463756611100315245ustar00rootroot00000000000000bootstrap_one.cfg000066400000000000000000000002411463756611100350010ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/onepuppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/spaces/000077500000000000000000000000001463756611100322215ustar00rootroot00000000000000bootstrap with spaces one.cfg000066400000000000000000000002411463756611100375720ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/spacespuppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service bootstrap with spaces two.cfg000066400000000000000000000002401463756611100376210ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/spacespuppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-three puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/two/000077500000000000000000000000001463756611100315545ustar00rootroot00000000000000bootstrap_two.cfg000066400000000000000000000002401463756611100350600ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cli/split_bootstraps/twopuppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-three puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cwd/000077500000000000000000000000001463756611100253365ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/cwd/bootstrap.cfg000066400000000000000000000002411463756611100300310ustar00rootroot00000000000000puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cwd-test-service puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/jar/000077500000000000000000000000001463756611100253355ustar00rootroot00000000000000this-jar-contains-a-bootstrap-config-file.jar000066400000000000000000000003651463756611100357260ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/jarPK⁞C#4ES bootstrap.cfgUT  R Rux 5K 0 KZ}h16![70*Ku +m O9TE¿J찫i[?PK⁞C#4ES bootstrap.cfgUT Rux PKSpuppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/plugin/000077500000000000000000000000001463756611100260575ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/bootstrapping/plugin/bootstrap.cfg000066400000000000000000000000671463756611100305600ustar00rootroot00000000000000test-services.plugin-test-services/plugin-test-service puppetlabs-trapperkeeper-d1f1135/dev-resources/config/000077500000000000000000000000001463756611100231335ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/config/conflictdir1/000077500000000000000000000000001463756611100255145ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/config/conflictdir1/config.conf000066400000000000000000000001101463756611100276200ustar00rootroot00000000000000foo { // comment somesetting : 12 # comment baz = "hi" }puppetlabs-trapperkeeper-d1f1135/dev-resources/config/conflictdir1/config.ini000066400000000000000000000000411463756611100274550ustar00rootroot00000000000000[foo] bar = "barbar" baz = bazbazpuppetlabs-trapperkeeper-d1f1135/dev-resources/config/conflictdir2/000077500000000000000000000000001463756611100255155ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/config/conflictdir2/config.json000066400000000000000000000000761463756611100276600ustar00rootroot00000000000000{"foo": {"something": "something", "baz": "jsonbaz"}}puppetlabs-trapperkeeper-d1f1135/dev-resources/config/conflictdir2/config.properties000066400000000000000000000000371463756611100311000ustar00rootroot00000000000000foo.bar="barbar" foo.baz=bazbazpuppetlabs-trapperkeeper-d1f1135/dev-resources/config/conflictdir3/000077500000000000000000000000001463756611100255165ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/config/conflictdir3/config.edn000066400000000000000000000000511463756611100274470ustar00rootroot00000000000000{:foo {:bar "barbar" :baz "bazbaz"}}puppetlabs-trapperkeeper-d1f1135/dev-resources/config/conflictdir3/config.json000066400000000000000000000000761463756611100276610ustar00rootroot00000000000000{"foo": {"something": "something", "baz": "jsonbaz"}}puppetlabs-trapperkeeper-d1f1135/dev-resources/config/file/000077500000000000000000000000001463756611100240525ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/config/file/config.conf000066400000000000000000000003141463756611100261640ustar00rootroot00000000000000foo { baz = "bazbaz" // this is a test comment bam: 42 # this is another test comment bap.boozle = "boozleboozle" } foo.bar = barbar foo.bap : { bip : [1, 2, { hi = "there" }, 3] }puppetlabs-trapperkeeper-d1f1135/dev-resources/config/file/config.edn000066400000000000000000000002011463756611100260000ustar00rootroot00000000000000{:foo {:bar "barbar" :baz "bazbaz" :bam 42 :bap {:boozle "boozleboozle" :bip [1 2 {:hi "there"} 3]}}}puppetlabs-trapperkeeper-d1f1135/dev-resources/config/file/config.ini000066400000000000000000000001251463756611100260160ustar00rootroot00000000000000[foo] # these are some settings setting1 = foo1 setting2=foo2 [bar] setting1 = bar1puppetlabs-trapperkeeper-d1f1135/dev-resources/config/file/config.json000066400000000000000000000002511463756611100262100ustar00rootroot00000000000000{"foo": {"bar": "barbar", "baz": "bazbaz", "bam": 42, "bap": {"boozle": "boozleboozle", "bip": [1, 2, {"hi": "there"}, 3] }}}puppetlabs-trapperkeeper-d1f1135/dev-resources/config/file/config.properties000066400000000000000000000001101463756611100274250ustar00rootroot00000000000000foo.bar="barbar" foo.baz=bazbaz foo.bam=42 foo.bap.boozle="boozleboozle"puppetlabs-trapperkeeper-d1f1135/dev-resources/config/inidir/000077500000000000000000000000001463756611100244115ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/config/inidir/bam.ini000066400000000000000000000000251463756611100256460ustar00rootroot00000000000000[bam] setting1 = bam1puppetlabs-trapperkeeper-d1f1135/dev-resources/config/inidir/baz.ini000066400000000000000000000000771463756611100256720ustar00rootroot00000000000000[baz] # these are some settings setting1 = baz1 setting2=baz2 puppetlabs-trapperkeeper-d1f1135/dev-resources/config/mixeddir/000077500000000000000000000000001463756611100247405ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/config/mixeddir/bar.conf000066400000000000000000000002251463756611100263520ustar00rootroot00000000000000bar { nesty.mappy { hi = there # comment stuff = [1, 2, {"how" = "areyou"}, 3] } // comment junk : "thingz" }puppetlabs-trapperkeeper-d1f1135/dev-resources/config/mixeddir/baz.ini000066400000000000000000000000771463756611100262210ustar00rootroot00000000000000[baz] # these are some settings setting1 = baz1 setting2=baz2 puppetlabs-trapperkeeper-d1f1135/dev-resources/config/mixeddir/foo.properties000066400000000000000000000000641463756611100276410ustar00rootroot00000000000000foo.bar="barbar" foo.baz=bazbaz foo.meaningoflife=42puppetlabs-trapperkeeper-d1f1135/dev-resources/config/mixeddir/taco.json000066400000000000000000000000711463756611100265570ustar00rootroot00000000000000{"taco": {"burrito": [1, 2], "nacho": "cheese"}}puppetlabs-trapperkeeper-d1f1135/dev-resources/logback.xml000066400000000000000000000005301463756611100240100ustar00rootroot00000000000000 %d %-5p [%c{2}] %m%n puppetlabs-trapperkeeper-d1f1135/dev-resources/logging/000077500000000000000000000000001463756611100233145ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/dev-resources/logging/logback-debug.xml000066400000000000000000000005731463756611100265310ustar00rootroot00000000000000 %d %-5p [%c{2}] %m%n puppetlabs-trapperkeeper-d1f1135/dev-resources/logging/logback-evaluator-filter.xml000066400000000000000000000025041463756611100307240ustar00rootroot00000000000000 matcher should get filtered matcher.matches(formattedMessage) NEUTRAL DENY omgMatcher OMGOMG omgMatcher.matches(throwable.getMessage()) NEUTRAL DENY ./target/test/logback-evaluator-filter-test.log false %d %-5p [%c{2}] %m%n puppetlabs-trapperkeeper-d1f1135/dev-resources/logging/logback-warn.xml000066400000000000000000000005721463756611100264110ustar00rootroot00000000000000 %d %-5p [%c{2}] %m%n puppetlabs-trapperkeeper-d1f1135/documentation/000077500000000000000000000000001463756611100217515ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/documentation/Bootstrapping.md000066400000000000000000000037041463756611100251320ustar00rootroot00000000000000# Bootstrapping As mentioned briefly on the [Quick Start](Trapperkeeper-Quick-Start.md) page, Trapperkeeper relies on a `bootstrap.cfg` file to determine the list of services that it should load at startup. The other piece of the bootstrapping equation is setting up a `main` that calls Trapperkeeper's bootstrap code. Here we'll go into a bit more detail about both of these topics. ## `bootstrap.cfg` The `bootstrap.cfg` file is a simple text file, in which each line contains the fully qualified namespace and name of a service. Here's an example `bootstrap.cfg` that enables the nREPL service and a custom `foo-service`: ``` puppetlabs.trapperkeeper.services.nrepl.nrepl-service/nrepl-service my.custom.namespace/foo-service ``` Note that it does not matter what order the services are specified in; trapperkeeper will resolve the dependencies between them, and start and stop them in the correct order based on their dependency relationships. In normal use cases, you'll want to simply put `bootstrap.cfg` in your `resources` directory and bundle it as part of your application (e.g. in an uberjar). However, there are cases where you may want to override the list of services (for development, customizations, etc.). To accommodate this, Trapperkeeper will actually search in three different places for the `bootstrap.cfg` file; the first one it finds will be used. Here they are, listed in order of precedence: * a location or list of locations ([see here](Command-Line-Arguments.md#multiple-bootstrap-files)) specified via the optional `--bootstrap-config` parameter on the command line when the application is launched * in the current working directory * on the classpath ## Configuration Bootstrapping determines _which_ services should be loaded, but it doesn't say _how_ they should be configured. For that, you'll want to learn about the [built-in service](Built-in-Services.md#configuration-service) that Trapperkeeper uses to read configuration data. puppetlabs-trapperkeeper-d1f1135/documentation/Built-in-Configuration-Service.md000066400000000000000000000104251463756611100301630ustar00rootroot00000000000000# Trapperkeeper's Built-in Configuration Service The configuration service is built-in to Trapperkeeper and is always loaded. It performs the following tasks at application startup: * Reads all application configuration into memory * Initializes logging * Provides functions that can be injected into other services to give them access to the configuration data In its current form, the configuration service has some fairly rigid behavior though in the future we hope to make it more dynamic. ## Loading configuration data All configuration data is read from config files on disk. When launching a Trapperkeeper application, you specify a `--config` command-line argument, whose value is a file path or comma-separated list of file paths. You may specify the path to a single config file, or you may specify a directory of config files. If no path is specified, Trapperkeeper will act as if you had passed it an empty configuration file. We support several types of files for expressing the configuration data: * `.ini` files * `.edn` files (Clojure's [Extensible Data Notation](https://github.com/edn-format/edn) format) * `.conf` files (this is the [Human-Optimized Config Object Notation](https://github.com/typesafehub/config/blob/master/HOCON.md) format; a flexible superset of JSON defined by the [typesafe config library](https://github.com/typesafehub/config)) * `.json` files * `.properties` files The configuration service will then parse the config file(s) into memory as a nested map; e.g., the section headers from an `.ini` file would become the top-level keys of the map, and the values will be maps containing the individual setting names and values from that section of the ini file. (If using `.edn`, `.conf`, or `.json`, you can control the nesting of the map more explicitly.) Here's the protocol for the configuration service: ```clj (defprotocol ConfigService (get-config [this] "Returns a map containing all of the configuration values") (get-in-config [this ks] [this ks default] "Returns the individual configuration value from the nested configuration structure, where ks is a sequence of keys. Returns nil if the key is not present, or the default value if supplied.")) ``` Your service may then specify a dependency on the configuration service in order to access service configuration data. Here's an example. Assume you have a directory called `conf.d`, and in it, you have a single config file called `foo.conf` with the following contents ```conf foosection1{ foosetting1 = foo foosetting2 = bar } ``` Then, you can define a service like this: ```clj (defservice foo-service [[:ConfigService get-in-config]] ;; service initialization code (init [this context] (println (format "foosetting2 has a value of '%s'" (get-in-config [:foosection1 :foosetting2]))) context)) ``` Then, if you add `foo-service` to your `bootstrap.cfg` file and launch your app with `--config ./conf.d`, during initialization of the `foo-service` you should see: foosetting2 has a value of 'bar' ## Logging configuration Trapperkeeper provides some automatic configuration for logging during application startup. This way, services don't have to deal with that independently, and all services running in the same Trapperkeeper container will be able to share a common logging configuration. The built-in logging configuration is compatible with `clojure.tools/logging`, so services can just call the `clojure.tools/logging` functions and logging will work out of the box. The logging implementation is based on [`logback`](http://logback.qos.ch/). This means that Trapperkeeper will look for a `logback.xml` file on the classpath, but you can override the location of this file via configuration. This is done using the configuration setting `logging-config` in a `global` section of your configuration files. `logback` is based on [`slf4j`](http://www.slf4j.org/), so it should be compatible with the built-in logging of just about any existing Java libraries that your project may depend on. For more information on configuring logback, have a look at [their documentation](http://logback.qos.ch/manual/configuration.html). For example: ```CONF global { logging-config = /path/to/logback.xml } ``` puppetlabs-trapperkeeper-d1f1135/documentation/Built-in-Services.md000066400000000000000000000022531463756611100255410ustar00rootroot00000000000000# Built-in Services Trapperkeeper includes a handful of built-in services that are intended to remove some of the tedium of tasks that are common to most applications. There is a configuration service (which is responsible for loading the application configuration and exposing it as data to other services), a shutdown service (which provides some means for shutting down the container and allows other services to register shutdown hooks), and an optional nREPL service (which can be used to run an embedded REPL in your application, so that you can connect to it from a remote process while it is running). There are some other basic services available that don't ship with the Trapperkeeper core, in order to keep the dependency tree to a minimum. Of particular interest is the [webserver service](https://github.com/puppetlabs/trapperkeeper-webserver-jetty9), which you can use to run clojure Ring applications or java servlets. Detailed information about Trapperkeeper's built-in services can be found on the following pages: - [Configuration Service](Built-in-Configuration-Service.md) - [Shutdown Service](Built-in-Shutdown-Service.md) - [nREPL Service](Built-in-nREPL-Service.md) puppetlabs-trapperkeeper-d1f1135/documentation/Built-in-Shutdown-Service.md000066400000000000000000000124411463756611100271670ustar00rootroot00000000000000# Trapperkeeper's Built-in Shutdown Service The shutdown service is built-in to Trapperkeeper and, like the [configuration service](Built-in-Configuration-Service.md), is always loaded. It has two main responsibilities: * Listen for a shutdown signal to the process, and initiate shutdown of the application if one is received (via CTRL-C or TERM signal) * Provide functions that can be used by other services to initiate a shutdown (either because of a normal application termination condition, or in the event of a fatal error) ## Shutdown Hooks A service may implement the `stop` function from the `Lifecycle` protocol. If so, this function will be called during application shutdown. The shutdown hook for any given service is guaranteed to be called *before* the shutdown hook for any of the services that it depends on. For example: ```clj (defn bar-shutdown [] (log/info "bar-service shutting down!")) (defservice bar-service [[:FooService foo]] ;; service initialization code (init [this context] (log/info "bar-service initializing.") context) ;; shutdown code (stop [this context] (bar-shutdown) context)) ``` Given this service definition, the `bar-shutdown` function would be called during shutdown of the Trapperkeeper container (during both a normal shutdown or an error shutdown). Because `bar-service` has a dependency on `foo-service`, Trapperkeeper would also guarantee that the `bar-shutdown` is called *prior to* the `stop` function for the `foo-service` (assuming `foo-service` provides one). ## Provided Shutdown Functions The shutdown service provides two functions that can be injected into other services: `request-shutdown` and `shutdown-on-error`. Here's the protocol: ```clj (defprotocol ShutdownService (request-shutdown [this] "Asynchronously trigger normal shutdown") (shutdown-on-error [this service-id f] [this service-id f on-error] "Higher-order function to execute application logic and trigger shutdown in the event of an exception")) ``` To use them, you may simply specify a dependency on them: ```clj (defservice baz-service [[:ShutdownService request-shutdown shutdown-on-error]] ;; ... ) ``` ### `request-shutdown` `request-shutdown` initiates a shutdown of the application container which will, in turn, cause all registered shutdown hooks to be called. It is asynchronous and will eventually cause the `run` function to return. It accepts an optional argument which can be used to provide a map specifying a process exit status and final messages like this: ```clj {:puppetlabs.trapperkepper.core/exit` {:status 3 :messages [["Unexpected filesystem error ..." *err*]]}} ``` which will finally be thrown from `run` as an `ex-info` of `:kind` `:puppetlabs.trapperkepper.core/exit` like this: ```clj {:kind :puppetlabs.trapperkepper.core/exit` :status 3 :messages [["Unexpected filesystem error ..." *err*]]}} ``` The `:messages` should include any desired newlines, and when relying on `:puppetlabs.trapperkepper.core/main`, the `:messages` will be printed and `exit` will be called with the given `:status`. ### `shutdown-on-error` `shutdown-on-error` is a higher-order function that can be used as a wrapper around some logic in your services; its functionality is simple: ```clj (try ; execute the given function (catch Throwable t ; initiate Trapperkeeper's shutdown logic ``` This has two main use-cases: * "worker" / background threads that your service may launch * a section of code that needs to execute in a service function, in which any error is so problematic that the entire application should shut down `shutdown-on-error` accepts either two or three arguments: `[service-id f]` or `[service-id f on-error-fn]`. `service-id` is the id of your service; you can retrieve this via `(service-id this)` inside of any of your service function definitions. `f` is a function containing whatever application logic you desire; this is the function that will be wrapped in `try/catch`. `on-error-fn` is an optional callback function that you can provide, which will be executed during error shutdown *if* an unhandled exception occurs during the execution of `f`. `on-error-fn` should take a single argument: `context`, which is the service context map (the same map that is used in the lifecycle functions). Here's an example: ```clj (defn my-work-fn [] ;; do some work (Thread/sleep 10000) ;; uh-oh! An unhandled exception! (throw (IllegalStateException. "egads!"))) (defn my-error-cleanup-fn [context] (log/info "Something terrible happened! Foo: " (context :foo)) (log/info "Performing shutdown logic that should only happen on a fatal error.")) (defn my-normal-shutdown-fn [] (log/info "Performing normal shutdown logic.")) (defservice yet-another-service [[:ShutdownService shutdown-on-error]] (init [this context] (assoc context :worker-thread (future (shutdown-on-error (service-id this) my-work-fn my-error-cleanup-fn)))) (stop [this context] (my-normal-shutdown-fn) context)) ``` In this scenario, the application would run for 10 seconds, and then the fatal exception would be thrown. Trapperkeeper would then call `my-error-cleanup-fn`, and then attempt to call all of the normal shutdown hooks in the correct order (including `my-normal-shutdown-fn`). puppetlabs-trapperkeeper-d1f1135/documentation/Built-in-nREPL-Service.md000066400000000000000000000032311463756611100262710ustar00rootroot00000000000000# Configuring the nREPL service The `nREPL` service is intended to be used as a debugging tool and not directly called by any other application code, so no useful functions are directly exported by this service. A `shutdown` function is provided solely to allow the shutdown service to cleanly stop the `nREPL` server. The `nrepl` section in a _Trapperkeeper_ configuration file specifies all the settings needed to start up an `nREPL` server attached to _Trapperkeeper_. ## `boostrap.cfg` By default, the nrepl service is not put into your application's `bootstrap.cfg`. If you want to use this service, add `puppetlabs.trapperkeeper.services.nrepl.nrepl-service/nrepl-service` to your `bootstrap.cfg` and enable it in your config. ## `enabled` The `enabled` flag is a boolean value, which can be set to either `"true"` or `"false"`. When this is set to true, the `nREPL` server will start and accept connections. If this value is not specified then `enabled=false` is assumed. ## `host` The IP address to bind the nREPL server to. If not specified then `0.0.0.0` is used, which indicates binding to all available interfaces. ## `port` The port that the `nREPL` server is bound to. If no port is defined then the default value of `7888` is used. ## `middlewares` A list of nREPL middlewares to load; for example, for compatibility with LightTable or other editors. ## Typical `config.conf` for nREPL ```conf nrepl { port = 12345 enabled = true middlewares = [lighttable.nrepl.handler/lighttable-ops] } ``` ## The `nREPL` server For more information on the nREPL server see [the nREPL server README](https://github.com/clojure/tools.nrepl/blob/master/README.md). puppetlabs-trapperkeeper-d1f1135/documentation/Command-Line-Arguments.md000066400000000000000000000216001463756611100265000ustar00rootroot00000000000000# Command Line Arguments Trapperkeeper's default mode of operation is to handle the processing of application command-line arguments for you. This is done for a few reasons: * It needs some data for bootstrapping * Since the idea is that you will be composing multiple services together in a Trapperkeeper instance, managing command line options across multiple services can be tricky; using the configuration service is easier * Who wants to process command-line arguments, anyway? Note that if you absolutely need control over the command line argument processing, it is possible to circumvent the built-in handling by calling Trapperkeeper's `bootstrap` function directly; see additional details in the [Bootstrapping](Bootstrapping.md) page. Trapperkeeper supports four command-line arguments: * `--config/-c`: The path to the configuration file or directory. This option is used to initialize the configuration service. This argument is optional; if not specified, Trapperkeeper will act as if you had given it an empty configuration file. * `--bootstrap-config/-b`: This argument is optional; if specified, the value should be a path to a bootstrap configuration file, or a comma separated list of files and directories ([see below](#multiple-bootstrap-files)) that Trapperkeeper will use (instead of looking for `bootstrap.cfg` in the current working directory or on the classpath) * `--debug/-d`: This option is not required; it's a flag, so it will evaluate to a boolean. If `true`, sets the logging level to DEBUG, and also sets the `:debug` key in the configuration map provided by the configuration-service. * `--restart-file/-r`: This argument is optional; if specified, the value should be a path to a file containing a start counter. Trapperkeeper increments this counter after each time it has started all of the services in an application. See the [Restart File](Restart-File.md) page for additional details. ### Multiple bootstrap files The `--bootstrap-config` argument can be used to specify multiple bootstrap files. This way, a Trapperkeeper app's bootstrap configuration can be split up into multiple locations. You might want to do this to separate logically related services into their own files for instance. If multiple bootstrap files are specified, Trapperkeeper will treat them as if they have all been concatenated into a single bootstrap.cfg file and handle dependency resolution as normal. Multiple bootstrap files are specified by giving the `--bootstrap-config` command line option a comma separated list of files and directories. For example: ``` --bootstrap-config ./first/path,/etc/second/path,./a/single/file.cfg ``` Each item in the list of paths can be one of: * A path to a single config file * A path to a directory of config files. Only files ending in .cfg will be used * A path to a file inside of a jar. E.g. `jar:file:///usr/bin/myjar.jar!/bootstrap.cfg` ## `main` and Trapperkeeper There are three different ways that you can initiate Trapperkeeper's bootstrapping process: ### Defer to Trapperkeeper's `main` function In your Leiningen project file, you can simply specify Trapperkeeper's `main` as your `:main`: :main puppetlabs.trapperkeeper.main Then you can simply use `lein run --config ...` to launch your app, or `lein uberjar` to build an executable jar file that calls Trapperkeeper's `main`. ### Call Trapperkeeper's `main` function from your code If you don't want to defer to Trapperkeeper as your `:main` namespace, you can simply call Trapperkeeper's `main` from your own code. All that you need to do is to pass along the command line arguments, which Trapperkeeper needs for initializing bootstrapping, configuration, etc. Here's what that might look like: ```clj (ns foo (:require [puppetlabs.trapperkeeper.core :as trapperkeeper])) (defn -main [& args] ;; ... any code you like goes here (apply trapperkeeper/main args)) ``` Trapperkeeper's `main` will call `exit` itself in some cases, e.g. after argument processing errors, `--help` requests, or calls to `request-shutdown` that specify a specific process exit status. ### Call Trapperkeeper's `run` function directly If your application needs to handle command line arguments directly, rather than allowing Trapperkeeper to handle them, you can circumvent Trapperkeeper's `main` function and call `run` directly. *NOTE* that if you intend to write multiple services and load them into the same Trapperkeeper instance, it can end up being tricky to deal with varying sets of command line options that are supported by the different services. For this reason, it is generally preferable to configure the services via the configuration files and not rely on command-line arguments. But, if you absolutely must... Here's how it can be done: ```clj (ns foo (:require [puppetlabs.trapperkeeper.core :as trapperkeeper])) (defn -main [& args] (let [my-processed-cli-args (process-cli-args args) trapperkeeper-options {:config (my-processed-cli-args :config-file-path) :bootstrap-config nil :debug false}] ;; ... other app initialization code (trapperkeeper/run trapperkeeper-options))) ``` Note that Trapperkeeper's `run` function requires a map as an argument, and this map must contain the `:config` key which Trapperkeeper will use just as it would have used the `--config` value from the command line. You may also (optionally) provide `:bootstrap-config` and `:debug` keys, to override the path to the bootstrap configuration file and/or enable debugging on the application. If shutdown is initiatiated by a call to `request-shutdown` asking for a specific exit status, `run` will throw an ex-info exception with a `:kind` of `puppetlabs.trapperkeeper.core/exit`. See the `request-shutdown` documentation for additional information. ### Other Ways to Boot We use the term `boot` to describe the process of building up an instance of a `TrapperkeeperApp`, and then calling `init` and `start` on all of its services in the correct order. It is possible to use the Trapperkeeper framework at a slightly lower level. Using `run` or `main` will boot all of the services and then block the main thread until a shutdown is triggered; if you need more control, you'll be getting a reference to a `TrapperkeeperApp` directly. #### `TrapperkeeperApp` protocol There is a protocol that represents a Trapperkeeper application: ```clj (defprotocol TrapperkeeperApp "Functions available on a Trapperkeeper application instance" (app-context [this] "Returns the application context for this app (an atom containing a map)") (check-for-errors! [this] (str "Check for any errors which have occurred in " "the bootstrap process. If any have " "occurred, throw a `java.lang.Throwable` with " "the contents of the error. If none have " "occurred, return the input parameter.") (init [this] "Initialize the services") (start [this] "Start the services") (stop [this] "Stop the services")) ``` With a reference to a `TrapperkeeperApp`, you can gain more control over when the lifecycle functions are called. To get an instance, you can call any of these functions: * `(boot-with-cli-data [cli-data])`: this function expects you to process your own command-line arguments into a map (as with `run`). It then creates a TrapperkeeperApp, boots all of the services, and returns the app. * `(boot-services-with-cli-data [services cli-data])`: this function expects you to process your own command-line arguments into a map, and also to build up your own list of services to pass in as the first argument. It circumvents the normal Trapperkeeper `bootstrap.cfg` process, creates a `TrapperkeeperApp` with all of your services, boots them, and returns the app. * `(boot-services-with-config [services config])`: this function expects you to process your own command-line arguments, configuration data, and build up your own list of services. You pass it the list of services and the map of all service configuration data, and it circumvents the normal `bootstrap.cfg` process, creates a `TrapperkeeperApp` with all of your services, boots them, and returns the app. Each of the above gives you a way to get a reference to a `TrapperkeeperApp` without blocking the main thread to wait for shutdown. If, later, you do wish to wait for the shutdown, you can simply call `run-app` and pass it your `TrapperkeeperApp`. Alternately, you can call `stop` on the `TrapperkeeperApp` to initiate shutdown on your own terms. Note that all of these functions *do* boot your services. If you wish to have more control over the booting of the services, you can use this function: * `(build-app [services config-data])`: this function creates a `TrapperkeeperApp` *without* booting the services. You can then boot them yourself by calling `init` and `start` on the `TrapperkeeperApp`. puppetlabs-trapperkeeper-d1f1135/documentation/Configuring-the-nREPL-Service.md000066400000000000000000000026151463756611100276430ustar00rootroot00000000000000# Configuring the nREPL service The `nREPL` service is intended to be used as a debugging tool and not directly called by any other application code. So no useful functions are directly exported by this service. A `shutdown` function is provided solely to allow the shutdown service to cleanly stop the `nREPL` server. The `[nrepl]` section in a _Trapperkeeper_ `.ini` configuration file specifies all the settings needed to start up an `nREPL` server attached to _Trapperkeeper_. ## `enabled` The `enabled` flag is a boolean value, which can be set to either `"true"` or `"false"`. When this is set to true, the `nREPL` server will start and accept connections. If this value is not specified then `enabled=false` is assumed. ## `host` The IP address to bind the nREPL server to. If not specified then `0.0.0.0` is used, which indicates binding to all available interfaces. ## `port` The port that the `nREPL` server is bound to. If no port is defined then the default value of `7888` is used. ## `middlewares` A list of nREPL middlewares to load; for example, for compatibility with LightTable or other editors. ## Typical `config.ini` for nREPL ```ini [nrepl] port = 12345 enabled = true middlewares = [lighttable.nrepl.handler/lighttable-ops] ``` ## The `nREPL` server For more information on the nREPL server, see [the tools.nrepl README](https://github.com/clojure/tools.nrepl/blob/master/README.md). puppetlabs-trapperkeeper-d1f1135/documentation/Defining-Services.md000066400000000000000000000202561463756611100256040ustar00rootroot00000000000000# Defining Services Trapperkeeper provides two constructs for defining services: `defservice` and `service`. As you might expect, `defservice` defines a service as a var in your namespace, and `service` allows you to create one inline and assign it to a variable in a let block or other location. Here's how they work: ## `defservice` `defservice` takes the following arguments: * a service name * an optional doc string * an optional service protocol; only required if your service exports functions that can be used by other services * a dependency list indicating other services/functions that this service requires * a series of function implementations. This must include all of the functions in the protocol if one is specified, and may also optionally provide override implementations for the built-in service `Lifecycle` functions. ### Service Lifecycle The service `Lifecycle` protocol looks like this: ```clj (defprotocol Lifecycle (init [this context]) (start [this context]) (stop [this context])) ``` (This may look familiar; we chose to use the same function names as some of the existing lifecycle protocols. Ultimately we'd like to just use one of those protocols directly, but for now our needs are different enough to warrant avoiding the introduction of a dependency on an existing project.) All service lifecycle functions are passed a service `context` map, which may be used to store any service-specific state (e.g., a database connection pool or some other object that you need to reference in subsequent functions.) Services may define these functions, `assoc` data into the map as needed, and then return the updated context map. The updated context map will be maintained by the framework and passed to subsequent lifecycle functions for the service. The default implementation of the lifecycle functions is to simply return the service context map unmodified; if you don't need to implement a particular lifecycle function for your service, you can simply omit it and the default will be used. Trapperkeeper will call the lifecycle functions in order based on the dependency list of the services; in other words, if your service has a dependency on service `Foo`, you are guaranteed that `Foo`'s `init` function will be called prior to yours, and that your `stop` function will be called prior to `Foo`'s. ### Example Service Let's look at a concrete example: ```clj ;; This is the list of functions that the `FooService` must implement, and which ;; are available to other services who have a dependency on `FooService`. (defprotocol FooService (foo1 [this x]) (foo2 [this]) (foo3 [this x])) (defservice foo-service ;; docstring (optional) "A service that foos." ;; now we specify the (optional) protocol that this service satisfies: FooService ;; the :depends value should be a vector of vectors. Each of the inner vectors ;; should begin with a keyword that matches the protocol name of another service, ;; which may be followed by any number of symbols. Each symbol is the name of a ;; function that is provided by that service. Trapperkeeper will fail fast at ;; startup if any of the specified dependency services do not exist, *or* if they ;; do not provide all of the functions specified in your vector. (Note that ;; the syntax used here is actually just the ;; [fnk binding syntax from the Plumatic plumbing library](https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk#fnk-syntax), ;; so you can technically use any form that is compatible with that.) [[:SomeService function1 function2] [:AnotherService function3 function4]] ;; After your dependencies list comes the function implementations. ;; You must implement all of the protocol functions (if a protocol is ;; specified), and you may also override any `Lifecycle` functions that ;; you choose. We'll start by implementing the `init` function from ;; the `Lifecycle`: (init [this context] ;; do some initialization ;; ... ;; now return the service context map; we can update it to include ;; some state if we like. Note that we can use the functions that ;; were specified in our dependency list here: (assoc context :foo (str "Some interesting state:" (function1))) ;; We could optionally also override the `start` and `stop` lifecycle ;; functions, but we won't for this example. ;; Now we'll define our service function implementations. Again, we are ;; free to use the imported functions from the other services here: (foo1 [this x] ((comp function2 function3) x)) (foo2 [this] (println "Function4 returns" (function4))) ;; We can also access the service context that we updated during the ;; lifecycle functions, by using the `service-context` function from ;; the `Service` protocol: (foo3 [this x] (let [context (service-context this)] (format "x + :foo is: '%s'" (str x (:foo context)))))) ``` After this `defservice` statement, you will have a var named `foo-service` in your namespace that contains the service. You can reference this from a Trapperkeeper bootstrap configuration file to include that service in your app, and once you've done that your new service can be referenced as a dependency (`{:depends [[:FooService ...`) by other services. ### Multi-arity Protocol Functions Clojure's protocols allow you to define multi-arity functions: ```clj (defprotocol MultiArityService (foo [this x] [this x y])) ``` Trapperkeeper services can use the syntax from clojure's `reify` to implement these multi-arity functions: ```clj (defservice my-service MultiArityService [] (foo [this x] x) (foo [this x y] (+ x y))) ``` ## `service` `service` works very similarly to `defservice`, but it doesn't define a var in your namespace; it simply returns the service instance. Here are some examples (with and without protocols): ```clj (service [] (init [this context] (println "Starting anonymous service!") context)) (defprotocol AnotherService (foo [this])) ``` ## Optional Services _Introduced in Trapperkeeper 1.2.0_ When defining a service, it is possible to mark certain other services your service depends on as being optional. This is useful, for example, when composing your service against services that you might not need during development or for certain deployment scenarios. You can write the same code whether or not an optional service has been included in your bootstrap.cfg or not. To mark a dependency as optional, you use a different form to specify your dependencies: ```clj (defprotocol HaikuService (get-haiku [this] "return a lovely haiku")) (defprotocol SonnetService (get-sonnet [this] "return a lovely sonnet")) ;; ... snip the definitions of HaikuService and SonnetService ... (defservice poetry-service PoetryService {:required [HaikuService] :optional [SonnetService]} (haiku [this] (get-haiku HaikuService)) (sonnet [this] (if-let [sonnet-svc (tk-svc/maybe-get-service this :SonnetService)] (get-sonnet sonnet-svc) "insert moving sonnet here")) ``` In the above example, we use a map of the form `{:required [...] :optional [...]}` to split up our required and optional dependencies. When we run this service in TK, our code will call `(get-sonnet)` if an implementation of `SonnetService` has been included in the `bootstrap.cfg`. Otherwise, we'll return the placeholder string `"insert moving sonnet here"`. **Warning** Because of a [limitation](https://github.com/plumatic/plumbing/issues/114) in Plumatic Schema, you can't use the destructuring `[:SonnetService get-sonnet]` syntax when declaring optional dependencies. The `Service` protocol has two helpers to make it easier to work with optional dependencies: * `(maybe-get-service [this service-id])` which takes a keyword service ID and returns the service, if included, or nil * `(service-included? [this service-id])` which takes a keyword service ID and returns true or false based on its inclusion. These helpers live alongside the other service helpers like `get-service` in `puppetlabs.trapperkeeper.services`. ## Referencing Services To learn how to refer to services in the rest of your application, head over to the [Referencing Services](Referencing-Services.md) page. puppetlabs-trapperkeeper-d1f1135/documentation/Error-Handling.md000066400000000000000000000041411463756611100251060ustar00rootroot00000000000000# Error Handling ## Errors During `init` or `start` If the `init` or `start` function of any service throws a `Throwable`, it will cause Trapperkeeper to shut down. No further `init` or `start` functions of any services will be called after the first `Throwable` is thrown. If you are using Trapperkeeper's `main` function, all service `stop` functions will be called before the process terminates. The `stop` functions are called in order to give each service a chance to clean up any resources which may have only been partially initialized before the `Throwable` was thrown -- e.g., allowing any worker threads which may have been spawned to be gracefully shut down so that the process can terminate. Service `stop` functions must be designed such that they could be executed with no adverse effects even if called before the service's `init` and `start` functions could successfully complete. If the `init` or `start` function of your service launches a background thread to perform some costly initialization computations (like, say, populating a pool of objects which are expensive to create), it is advisable to wrap that computation inside a call to `shutdown-on-error`; however, you should note that `shutdown-on-error` does *not* short-circuit Trapperkeeper's start-up sequence - the app will continue booting. The `init` and `start` functions of all services will still be run, and once that has completed, all `stop` functions will be called, and the process will terminate. ## Services Should Fail Fast Trapperkeeper embraces fail-fast behavior. With that in mind, we advise writing services that also fail-fast. In particular, if your service needs to spin-off a background thread to perform some expensive initialization logic, it is a best practice to push as much code as possible outside of the background thread (for example, validating configuration data), because `Throwables` on the main thread will propagate out of `init` or `start` and cause the application to shut down - i.e., it will *fail fast*. There are different operational semantics for errors thrown on a background thread (see previous section). puppetlabs-trapperkeeper-d1f1135/documentation/Helpful-Leiningen-Features.md000066400000000000000000000026311463756611100273560ustar00rootroot00000000000000# Helpful Leiningen Features There's nothing really special about developing a Trapperkeeper application as compared to any other Clojure application, but there are a couple of things we've found useful: ### Leiningen's `checkouts` feature Since Trapperkeeper is intended to help modularize applications, it also increases the likelihood that you'll end up working with more than one code base/git repo at the same time. When you find yourself in this situation, Leiningen's [checkouts](http://jakemccrary.com/blog/2012/03/28/working-on-multiple-clojure-projects-at-once/) feature is very useful. ### Leiningen's `trampoline` feature If you need to test the shutdown behavior of your application, you may find yourself trying to do `lein run` and then sending a CTRL-C or `kill`. However, due to the way Leiningen manages JVM processes, this CTRL-C will be handled by the lein process and won't actually make it to Trapperkeeper. If you need to test shutdown functionality, you'll want to use `lein trampoline run`. However, one quirk that we've discovered is that it does not appear that lein's `checkouts` and `trampoline` features work together; thus, when you run the app via `lein trampoline`, the classpath will not include the projects in the `checkouts` directory. Thus, you'll need to do `lein install` on the `checkouts` projects to copy their jars into your `.m2` directory before running `lein trampoline run`. puppetlabs-trapperkeeper-d1f1135/documentation/Index.md000066400000000000000000000051121463756611100233410ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/puppetlabs/trapperkeeper.png?branch=master)](https://travis-ci.org/puppetlabs/trapperkeeper) Trapperkeeper is a Clojure framework for hosting long-running applications and services. You can think of it as a sort of "binder" for Ring applications and other modular bits of Clojure code. * [Overview](Overview.md) * [Credits and Origins](Overview.md#credits-and-origins) * [Hopes and Dreams](Overview.md#hopes-and-dreams) * [Trapperkeeper Quick Start](Trapperkeeper-Quick-Start.md) * [Leiningen Template](Trapperkeeper-Quick-Start.md#lein-template) * [Hello World](Trapperkeeper-Quick-Start.md#hello-world) * [Defining Services](Defining-Services.md) * [`defservice`](Defining-Services.md#defservice) * [Service Lifecycle](Defining-Services.md#service-lifecycle) * [Example Service](Defining-Services.md#example-service) * [Multi-arity Protocol Functions](Defining-Services.md#multi-arity-protocol-functions) * [`service`](Defining-Services.md#service) * [Optional Services](Defining-Services.md#optional-services) * [Referencing Services](Referencing-Services.md) * [Individual Functions](Referencing-Services.md#individual-functions) * [A Map of Functions](Referencing-Services.md#a-map-of-functions) * [Plumatic Graph Binding Form](Referencing-Services.md#plumatic-graph-binding-form) * [Via Service Protocol](Referencing-Services.md#via-service-protocol) * [Bootstrapping](Bootstrapping.md) * [Built-in Services](Built-in-Services.md) * [Configuration Service](Built-in-Configuration-Service.md) * [Shutdown Service](Built-in-Shutdown-Service.md) * [nREPL Service](Built-in-nREPL-Service.md) * [Error Handling](Error-Handling.md) * [Service Interfaces](Service-Interfaces.md) * [Command Line Arguments](Command-Line-Arguments.md) * [Other Ways to Boot](Command-Line-Arguments.md#other-ways-to-boot) * [Restart File Feature for Determining When Services Have Been Started](Restart-File.md) * [Test Utils](Test-Utils.md) * [Trapperkeeper Best Practices](Trapperkeeper-Best-Practices.md) * [To Trapperkeeper Or Not To Trapperkeeper](Trapperkeeper-Best-Practices.md#to-trapperkeeper-or-not-to-trapperkeeper) * [Separating Logic From Service Definitions](Trapperkeeper-Best-Practices.md#separating-logic-from-service-definitions) * [On Lifecycles](Trapperkeeper-Best-Practices.md#on-lifecycles) * [Testing Services](Trapperkeeper-Best-Practices.md#testing-services) * [Using the "Reloaded" Pattern](Reloaded-Pattern.md) * [Experimental Plugin System](Plugin-System.md) * [Polyglot Support](Polyglot-Support.md) * [Helpful Leiningen Features](Helpful-Leiningen-Features.md) puppetlabs-trapperkeeper-d1f1135/documentation/Overview.md000066400000000000000000000111131463756611100240760ustar00rootroot00000000000000# Overview Trapperkeeper is a Clojure framework for hosting long-running applications and services. You can think of it as a "binder", of sorts--for Ring applications and other modular bits of Clojure code. It ties together a few nice patterns we've come across in the clojure community: * Stuart Sierra's ["reloaded" workflow](http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded) * Component lifecycles (["Component"](https://github.com/stuartsierra/component), ["jig"](https://github.com/juxt/jig#components)) * [Composable services](http://plumatic.github.io/graph-abstractions-for-structured-computation/) (based on the excellent [Plumamatic graph library](https://github.com/plumatic/plumbing)) We also had a few other needs that Trapperkeeper addresses (some of these arise because of the fact that we at Puppet Labs are shipping on-premises software, rather than SaaS. The framework is a shipping part of the application, in addition to providing useful features for development): * Well-defined service interfaces (using clojure protocols) * Ability to turn services on and off via configuration after deploy * Ability to swap service implementations via configuration after deploy * Ability to load multiple web apps (usually Ring) into a single webserver * Unified initialization of logging and configuration so services don't have to concern themselves with the implementation details * Super-simple configuration syntax A "service" in Trapperkeeper is represented as simply a map of clojure functions. Each service can advertise the functions that it provides via a protocol, as well as list other services that it has a dependency on. You then configure Trapperkeeper with a list of services to run and launch it. At startup, it validates that all of the dependencies are met and fails fast if they are not. If they are, then it injects the dependency functions into each service and starts them all up in the correct order. Trapperkeeper provides a few built-in services such as a configuration service, a shutdown service, and an nREPL service. Other services (such as a web server service) are available and ready to use, but don't ship with the base framework. Your custom services can specify dependencies on these and leverage the functions that they provide. For more details, see the [Built-in Services](Built-in-Services.md) page. # Credits and Origins Most of the heavy-lifting of the Trapperkeeper framework is handled by the excellent [Plumatic Graph](https://github.com/plumatic/plumbing) library. To a large degree, Trapperkeeper just wraps some basic conventions and convenience functions around that library, so many thanks go out to the fine folks at Plumatic for sharing their code! Trapperkeeper borrows some of the most basic concepts of the OSGi "service registry" to allow users to create simple "services" and bind them together in a single container, but it doesn't attempt to do any fancy classloading magic, hot-swapping of code at runtime, or any of the other things that can make OSGi and other similar application frameworks complex to work with. # Hopes and Dreams Here are some ideas that we've had and things we've played around with a bit for improving Trapperkeeper in the future. ## More flexible configuration service The current configuration service is hard-coded to use files (`.ini`, `.edn`, `.conf`, `.json`, or `.properties`) as its back end and is hard-coded to use `logback` to initialize logging. We'd like to make all of those more flexible; e.g., to support other persistence mechanisms, perhaps allow dynamic modifications to configuration values, support other logging frameworks, etc. These changes will probably require us to make the service life cycle just a bit more complex, though, so we didn't tackle them for the initial releases. ## Alternate implementations of the webserver service We currently provide both a [Jetty 7](https://github.com/puppetlabs/trapperkeeper-webserver-jetty7) and a [Jetty 9](https://github.com/puppetlabs/trapperkeeper-webserver-jetty9) implementation of the web server service. We may also experiment with some other options such as Netty. ## Add support for other types of web applications The current `:webserver-service` interface provides functions for registering a [Ring](https://github.com/ring-clojure/ring) or [Servlet](http://docs.oracle.com/javaee/7/api/javax/servlet/Servlet.html) application. We'd like to add a few more similar functions that would allow you to register other types of web applications, specifically an `add-rack-handler` function that would allow you to register a Rack application (to be run via JRuby). puppetlabs-trapperkeeper-d1f1135/documentation/Plugin-System.md000066400000000000000000000023031463756611100250110ustar00rootroot00000000000000# Experimental Plugin System Trapperkeeper has an **extremely** simple, experimental plugin mechanism. It allows you to specify (as a command-line argument) a directory of "plugin" .jars that will be dynamically added to the classpath at runtime. Each jar file will also be checked for duplicate classes or namespaces before it is added, so as to prevent any unexpected behavior. This provides the ability to extend the functionality of a deployed, Trapperkeeper-based application by simply including one or more services packaged into standalone "plugin" jar files, and adding the additional service(s) to the bootstrap configuration. Projects that wish to package themselves as "plugin" jar files should build an uberjar containing all of their dependencies. However, there is one caveat here - Trapperkeeper *and all of its dependencies* should be excluded from the uberjar. If the exclusions are not defined correctly, Trapperkeeper will fail to start because there will be duplicate versions of classes/namespaces on the classpath. Plugins are specified via a command-line argument: `--plugins /path/to/plugins/directory`; every .jar file in that directory will be added to the classpath by Trapperkeeper. puppetlabs-trapperkeeper-d1f1135/documentation/Polyglot-Support.md000066400000000000000000000025731463756611100255650ustar00rootroot00000000000000# Polyglot Support It should be possible (when extenuating circumstances necessitate it) to integrate code from just about any JVM language into a Trapperkeeper application. At the time of this writing, the only languages we've really experimented with are Java and Ruby (via JRuby). For Java, the Trapperkeeper webserver service contains an [example servlet app](https://github.com/puppetlabs/trapperkeeper-webserver-jetty9/tree/master/examples/servlet_app), which illustrates how you can run a Java servlet in trapperkeeper's webserver. We have also included a simple example of wrapping a Java library in a Trapperkeeper service, so that it can provide functions to other services. Have a look at the code for the [example Java service provider app](https://github.com/puppetlabs/trapperkeeper/tree/master/examples/java_service) for more info. For Ruby, we've been able to write an alternate implementation of a `webserver-service` which provides an `add-rack-handler` function for running Rack applications inside of Trapperkeeper. We've also been able to illustrate the ability to call clojure functions provided by existing clojure Trapperkeeper services from the Ruby code in such a Rack application. This code isn't necessarily production quality yet, but if you're interested, have a look at the [trapperkeeper-ruby project on github](https://github.com/puppetlabs/trapperkeeper-ruby). puppetlabs-trapperkeeper-d1f1135/documentation/Referencing-Services.md000066400000000000000000000060661463756611100263130ustar00rootroot00000000000000# Referencing Services One of the most important features of Trapperkeeper is the ability to specify dependencies between services, and, thus, to reference functions provided by one service from functions in another service. Trapperkeeper actually exposes several different ways to reference such functions, since the use cases may vary a great deal depending on the particular services involved. ## Individual Functions In the simplest case, you may just want to grab a direct reference to one or more individual functions from another service. That can be accomplished like this: ```clj (defservice foo-service [[:BarService bar-fn] [:BazService baz-fn]] (init [this context] (bar-fn) (baz-fn) context)) ``` This form expresses a dependency on two other services; one implementing the `BarService` protocol, and one implementing the `BazService` protocol. It gives us a direct reference to the functions `bar-fn` and `baz-fn`. You can call them as normal functions, without worrying about protocols any further. ## A Map of Functions If you want to get simple references to plain-old functions from a service (again, without worrying about the protocols), but you don't want to have to list them all out explicitly in the binding form, you can do this: ```clj (defservice foo-service [BarService BazService] (init [this context] ((:bar-fn BarService)) ((:baz-fn BazService)) context)) ``` With this syntax, what you get access to are two local vars `BarService` and `BazService`, the value of each of which is a map. The map keys are all keyword versions of the function names for all of the functions provided by the service protocol, and the values are the plain-old functions that you can just call directly. ## Plumatic Graph Binding Form Both of the cases above are actually just specific examples of forms supported by the underlying Plumatic Graph library that we are using to manage dependencies. If you're interested, the plumatic library offers some other ways to specify the binding forms and access your dependencies. For more info, see the [fnk binding syntax from the Plumatic plumbing library](https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk#fnk-syntax). ## Via Service Protocol In some cases you may actually prefer to get a reference to an object that satisfies the service protocol. This way, you can pass the object around and use the actual clojure protocol to reference the functions provided by a service. To achieve this, you use the `get-service` function from the main `Service` protocol. Here's how this might look: ```clj (ns bar.service) (defprotocol BarService (bar-fn [this])) ... (ns foo.service (:require [bar.service :as bar])) (defservice foo-service ;; This dependency is only here to enforce that the BarService gets loaded ;; before this service does; we won't need to refer to the `BarService` var ;; anywhere in this service definition. [BarService] (init [this context] (let [bar-service (get-service this :BarService)] (bar/bar-fn bar-service)) context)) ``` puppetlabs-trapperkeeper-d1f1135/documentation/Reloaded-Pattern.md000066400000000000000000000045651463756611100254370ustar00rootroot00000000000000# Using the "Reloaded" Pattern [Stuart Sierra's "reloaded" workflow](http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded) has become very popular in the clojure world of late; and for good reason, it's an awesome and super-productive way to do interactive development in the REPL, and it also helps encourage code modularity and minimizing mutable state. He has some [example code](https://github.com/stuartsierra/component#reloading) that shows some utility functions to use in the REPL to interact with your application. Trapperkeeper was designed with this pattern in mind as a goal. Thus, it's entirely possible to write some very similar code that allows you to start/stop/reload your app in a REPL: ```clj (ns examples.my-app.repl (:require [puppetlabs.trapperkeeper.services.webserver.jetty9-service :refer [jetty9-service]] [examples.my-app.services :refer [count-service foo-service baz-service]] [puppetlabs.trapperkeeper.core :as tk] [puppetlabs.trapperkeeper.app :as tka] [clojure.tools.namespace.repl :refer (refresh)])) ;; a var to hold the main `TrapperkeeperApp` instance. (def system nil) (defn init [] (alter-var-root #'system (fn [_] (tk/build-app [jetty9-service count-service foo-service baz-service] {:global {:logging-config "examples/my_app/logback.xml"} :webserver {:port 8080} :example {:my-app-config-value "FOO"}}))) (alter-var-root #'system tka/init) (tka/check-for-errors! system)) (defn start [] (alter-var-root #'system (fn [s] (if s (tka/start s)))) (tka/check-for-errors! system)) (defn stop [] (alter-var-root #'system (fn [s] (when s (tka/stop s))))) (defn go [] (init) (start)) (defn context [] @(tka/app-context system)) ;; pretty print the entire application context (defn print-context [] (clojure.pprint/pprint (context))) (defn reset [] (stop) (refresh :after 'examples.my-app.repl/go)) ``` For a working example, see the `repl` namespace in the [jetty9 example app](https://github.com/puppetlabs/trapperkeeper-webserver-jetty9/tree/master/examples/ring_app). puppetlabs-trapperkeeper-d1f1135/documentation/Restart-File.md000066400000000000000000000056311463756611100246010ustar00rootroot00000000000000# Experimental Feature: Restart File When using Trapperkeeper apps inside of packages, it is convenient for a service framework to have a clear indication as to when all of the Trapperkeeper services in the app have been started -- as opposed to just knowing when the Java process hosting the app has been spawned. The "restart file" feature in Trapperkeeper provides this capability. As a reference example, the [EZBake](https://github.com/puppetlabs/ezbake) build system for Trapperkeeper-based applications makes use of the "restart file" feature. Its service packages can pause a hosting service framework (SysVinit, systemd) during a "service start" attempt until the app's services have all been started, or have failed to start. The "restart file" is considered to be a somewhat experimental feature in that the implementation may change in a future release. ## Implementation Details Each time Trapperkeeper has successfully finished processing all of the start calls that it makes to each of the services in an application -- both at Java process start and after a service reload is requested -- it increments a counter in a file on disk. The location of the file is controlled by the value of the `restart-file` setting. If the value in the file before services are started is '3', for example, the value will be updated to '4' after services have been started. If the file does not exist at the time services have been started, the value is written as '1'. The value rolls back around to '1' if the value would be incremented beyond the maximum value for a `java.lang.Long` or if the contents of the file is otherwise unable to be parsed as an integer. In terms of using the restart file as an indication that services have been started -- for example, from a background script that accesses the file in a polling loop to determine when the start phase has finished -- it is best to just look for a change to the contents of the file rather than having any specific logic that interprets the integer values. As noted earlier, the nature of the 'start' marker may change in a future release. ## Configuration Details The `restart-file` setting can be specified either via a command line argument to Trapperkeeper... ``` -r | --restart-file /write/file/here ``` ... or as a setting under the "global" section of a Trapperkeeper configuration file. For example, a HOCON-formatted "global.conf" might have: ``` global: { restart-file: /write/file/here } ``` In the event that the `restart-file` setting were specified both as a command line argument and within the "global" section of a Trapperkeeper configuration file, the value specified on the command line would be the one in which the 'start' counter is incremented. If a value for the `restart-file` setting is not specified via either the command line or within the "global" section of a Trapperkeeper configuration file, Trapperkeeper will not write a 'start' counter to any file. puppetlabs-trapperkeeper-d1f1135/documentation/Service-Interfaces.md000066400000000000000000000070741463756611100257640ustar00rootroot00000000000000## Service Interfaces One of the goals of Trapperkeeper's "service" model is that a service should be thought of as simply an interface; any given service provides a protocol as its "contract", and the implementation details of these functions are not important to consumers. (This borrows heavily from OSGi's concept of a "service".) This means that you can write multiple implementations of a given service and swap them in and out of your application by simply modifying your configuration, without having to change any of the consuming code. The Trapperkeeper `webserver` service is an example of this pattern; we provide both a [Jetty 7 webserver service](https://github.com/puppetlabs/trapperkeeper-webserver-jetty7) and a [Jetty 9 webserver service](https://github.com/puppetlabs/trapperkeeper-webserver-jetty9) that can be used interchangeably. One of the motivations behind this approach is to make it easier to ship "on-premise" or "shrink-wrapped" software written in Clojure. In SaaS environments, the developers and administrators have tight control over what components are used in an application, and can afford to be fairly rigid about how things are deployed. For on-premise software, the end user may need to have a great deal more control over how components are mixed and matched to provide a solution that scales to meet their needs; for example, a small shop may be able to run 10 services on a single machine without approaching the load capacity of the hardware, but a slightly larger shop might need to separate those services out onto multiple machines. Trapperkeeper provides an easy way to do this at packaging time or configuration time, and the administrator does not necessarily have to be familiar with clojure or EDN in order to effectively configure their system. Here's a concrete example of how this might work: ```clj (ns services.foo) (defprotocol FooService (foo [this])) (ns services.foo.lowercase-foo (:require [services.foo :refer [FooService]) (defservice foo-service "A lower-case implementation of the `foo-service`" FooService [] (foo [this] "foo")) (ns services.foo.uppercase-foo (:require [services.foo :refer [FooService])) (defservice foo-service "An upper-case implementation of the `foo-service`" FooService [] (foo [this] "FOO")) (ns services.foo-consumer) (defprotocol FooConsumer (bar [this])) (defservice foo-consumer "A service that consumes the `foo-service`" FooConsumer [[:FooService foo]] (bar [this] (format "Foo service returned: '%s'" (foo)))) ``` Given this combination of services, you might have a `bootstrap.cfg` file that looks like:
services.foo-consumer/foo-consumer
services.foo.lowercase-foo/foo-service
If you then ran your app, calling the function `bar` provided by the `foo-consumer` service would yield: `"Foo service returned 'foo'"`. If you then modified your `bootstrap.cfg` file to look like:
services.foo-consumer/foo-consumer
services.foo.uppercase-foo/foo-service
Then the `bar` function would return `"Foo service returned 'bar'"`. This allows you to swap out a service implementation without making any code changes; you need only modify your `bootstrap.cfg` file. This is obviously a trivial example, but the same approach could be used to swap out the implementation of something more interesting; a webserver, a message queue, a persistence layer, etc. This also has the added benefit of helping to keep code more modular; a downstream service should only interact with a service that it depends on through a well-known interface. puppetlabs-trapperkeeper-d1f1135/documentation/Test-Utils.md000066400000000000000000000152461463756611100243200ustar00rootroot00000000000000# Trapperkeeper Test Utils Trapperkeeper provides some [utility code](https://github.com/puppetlabs/trapperkeeper/tree/master/test/puppetlabs/trapperkeeper/testutils) for use in tests. The code is available in a separate "test" jar that you may depend on by using a classifier in your project dependencies. ```clojure (defproject yourproject "1.0.0" ... :profiles {:dev {:dependencies [[puppetlabs/trapperkeeper "x.y.z" :classifier "test"]]}}) ``` ## Logging The [logging namespace](https://github.com/puppetlabs/trapperkeeper/tree/master/test/puppetlabs/trapperkeeper/testutils/logging.clj) provides utilities to help capture and validate logging behavior. ### `with-test-logging` This form provides one of the simplest, though least discriminating ways to examine the log events produced by a body of code. All log events generated by the "root" logger from within the form (typically all events) will be available for inspection by the `logged?` predicate: ```clojure (with-test-logging (log/info "hello log") (is (logged? #"^hello log$")) (is (logged? #"^hello log$" :info))) ``` Here `(log/info "hello log")` generates an info level log event with a message of "hello log", and then `logged?` checks for it, first by matching the message, and then by matching both the message and the level. ### `logged?` `logged?` must be called from within a `with-test-logging` form, and returns true if any events that match its arguments have been logged since the beginning of the form. See the `logged?` docstring for a complete description, but as an example, if the first argument is a regex pattern (typically generated via Clojure's `#"pattern"`), then `logged?` will return true if the pattern matches a single message of anything that has been logged since the beginning of the enclosing `with-test-logging` form. An optional second parameter restricts the match to log events with the specified level: `:trace`, `:debug`, `:info`, `:warn`, `:error` or `:fatal`. Note: by default `logged?` returns true only if there is exactly one log line match. An optional third parameter can be specified to disable this restriction. ### `event->map` This function converts a LogEvent to a Clojure map of the kind generated by `with-logged-event-maps` and `with-logger-event-maps`. A log event produced by `(log/info "hello log")` would be converted to this: ```clojure {:message "hello log" :level :info :exception nil :logger "the.namespace.containing.the.log.info.call"} ``` ### `with-logged-event-maps` This form provides more control than `with-test-logging` by appending an `event->map` map to a collection for each log event produced within its body, and the collection can be accessed though an atom bound to a caller-specified name. For example, the `with-test-logging` based tests above could be rewritten like this: ```clojure (with-logged-event-maps events (log/info "hello log") (is (some #(re-matches #"hello log" (:message %)) @events)) (is (some #(and (re-matches #"hello log" (:message %)) (= :info (:message %))) @events))) ``` A call to `(with-logged-event-maps ...)` is effectively the same as `(with-logger-event-maps root-logger-name ...)`. ### `with-logger-event-maps` This form is identical to `with-logged-event-maps` except that it allows the specification of the `logger-id` from which events should be captured; `with-logged-event-maps` always captures events from `root-logger-name`. ## Testing Services For the most part, we recommend that Trapperkeeper service definitions be written as thin wrappers around plain old functions. This means that the vast majority of your tests can be written as unit tests that operate on those functions directly. However, it can be useful to have a few tests that actually boot up a Trapperkeeper application instance; this allows you to, for example, verify that the services that you have a dependency on get injected correctly. To this end, the `puppetlabs.trapperkeeper.testutils.bootstrap` namespace includes some helper functions and macros for creating a Trapperkeeper application. The macros should be preferred in most cases; they generally start with the prefix `with-app-`, and allow you to create a temporary Trapperkeeper app given a list of services. They will take care of some important mechanics for you: * Making sure that no JVM shutdown hooks are registered during tests, as they would be during a normal Trapperkeeper application boot sequence * Making sure that the app is shut down properly after the test completes. Here are some of the most useful ones: ### `with-app-with-config` This macro allows you to specify the services you want to launch directly and to pass in a map of configuration data that the app should use. The services specified must include all dependencies and transitive dependencies needed to start each service; that is, what you'd normally put in the bootstrap.cfg. ```clj (ns services.test-service-1) (defprotocol TestService1 (test-fn [this])) (defservice test-service1 TestService1 [] (test-fn [this] "foo")) ``` ```clj (ns services.test-service2) (defservice test-service2 ;;... ) ``` ```clj (ns test.services-test (:require services.test-service-1 :as t1)) (with-app-with-config app [test-service1 test-service2] {:myconfig {:foo "foo" :bar "bar"}} (let [test-svc (get-service app :TestService1)] (is (= "baz" (t1/test-fn test-svc)))) ``` ### `with-app-with-cli-data` This variant is very similar, but instead of passing a map of config data, you pass a map of parsed command-line arguments, such as the path to a config file on disk that should be processed to build the actual application configuration: ```clj (with-app-with-cli-data app [test-service1 test-service2] {:config "./dev-resources/config.conf"} (let [test-svc (get-service app :TestService1)] (is (= "baz" (t1/test-fn test-svc)))) ``` ### `with-app-with-cli-args` This version accepts a vector of command-line arguments: ```clj (with-app-with-cli-args app [test-service1 test-service2] ["--config" "./dev-resources/config.conf" "--debug"] (let [test-svc (get-service app :TestService1)] (is (= "baz" (t1/test-fn test-svc)))) ``` ### `with-app-with-empty-config` This version is useful when you don't need to pass in any configuration data at all to the services: ```clj (with-app-with-empty-config app [test-service1 test-service2] (let [test-svc (get-service app :TestService1)] (is (= "baz" (t1/test-fn test-svc)))) ``` For each of the above macros, there is generally a `bootstrap-services-with-*` function that will behave similarly; however, the `bootstrap-*` functions don't handle the cleanup/shutdown behaviors for you, so they should only be used in rare cases. puppetlabs-trapperkeeper-d1f1135/documentation/Trapperkeeper-Best-Practices.md000066400000000000000000000110071463756611100277110ustar00rootroot00000000000000# Trapperkeeper Best Practices This page provides some general guidelines for writing Trapperkeeper services. ## To Trapperkeeper Or Not To Trapperkeeper Trapperkeeper gives us a lot of flexibility on how we decide to package and deploy applications and services. When should you use it? The easiest rule of thumb is: if it's possible to expose your code as a simple library with no dependencies on Trapperkeeper, it's highly preferable to go that route. Here are some things that might be reasonable indicators that you should consider exposing your code via a Trapperkeeper service: * You're writing a clojure web service and there is a greater-than-zero percent chance that you will eventually want to be able to run it inside of the same embedded web server instance as another web service. * Your code initializes some long-lived, stateful resource that needs to be used by other code, and that other code might not want/need to be responsible for explicitly managing the lifecycle of your resource * Your code has a need for a managed lifecycle; initialization / startup, shutdown / cleanup * Your code has a dependency on some other code that has a managed lifecycle * Your code requires external configuration that you would like to make consistent with other puppetlabs / Trapperkeeper applications ## Separating Logic From Service Definitions In general, it's a good idea to keep the code that implements your business logic completely separated from the Trapperkeeper service binding. This makes it much easier to test your functions as functions, without the need to boot up the whole framework. It also makes your code more re-usable and portable. Here's a more concrete example: *DON'T DO THIS:* ```clj (defprotocol CalculatorService (add [this x y])) (defservice calculator-service CalculatorService [] (add [this x y] (+ x y))) ``` This is better: ```clj (ns calculator.core) (defn add [x y] (+ x y)) ``` ```clj (ns calculator.service (:require calculator.core :as core)) (defprotocol CalculatorService (add [this x y])) (defservice calculator-service CalculatorService [] (add [this x y] (core/add x y))) ``` This way, you can test `calculator.core` directly, and re-use the functions it provides in other places without having to worry about Trapperkeeper. ## On Lifecycles Trapperkeeper provides three lifecycle functions: init, start, and stop. Hopefully "stop" is pretty obvious. We've had some questions, though, about what the difference is between "init" and "start". Trapperkeeper doesn't impose a hard-and-fast rule that you must follow for how you use these, but here are some data points: * The 'init' function of any service that you depend on will always be called before your 'init', and before any 'start'. The 'start' function of any service that you depend on will always be called before your 'start'. * Trapperkeeper itself doesn't impose any semantics about what kinds of things you should do in each of those lifecycle phases. It's more about giving services the flexibility to establish a contract with other services. For example, a webserver service may specify that it only accepts the registration of web handlers during the 'init' phase, and that no new handlers can be added after it has completed its 'start' phase. (This is just a theoretical example; this restriction isn't actually true for our current jetty implementations.) * The lifecycles are relatively new; as people start to use these lifecycles a bit more, we may end up shaking out a more concrete best-practice pattern. It's also possible we might end up introducing another phase or two to give more granularity... for now, we wanted to try to keep it fairly simple and flexible, and get a handle on what kinds of use cases people end up having for it. ## Testing Services As we mentioned before, it's better to separate your business logic from your service definitions as much as possible, so that you can test your business logic functions directly. Thus, the vast majority of your tests should not need to involve Trapperkeeper at all. However, you probably will want to have a small handful of tests that do boot up a full Trapperkeeper app, so that you can verify that your dependencies work as expected, etc. When writing tests that boot a Trapperkeeper app, the best way to do it is to use the helper testutils macros that we describe in the [testutils documentation](Test-Utils.md). They will take care of things like making sure the application is shut down cleanly after the test, and will generally just make your life easier. puppetlabs-trapperkeeper-d1f1135/documentation/Trapperkeeper-Quick-Start.md000066400000000000000000000046521463756611100272600ustar00rootroot00000000000000# Trapperkeeper Quick Start ## Lein Template A Leiningen template is available that shows a suggested project structure: lein new trapperkeeper my.namespace/myproject Once you've created a project from the template, you can run it via the lein alias: lein tk Note that the template is not intended to suggest a specific namespace organization; it's just intended to show you how to write a service, a web service, and tests for each. ## Hello World Here's a "hello world" example for getting started with Trapperkeeper. First, you need to define one or more services: ```clj (ns hello (:require [puppetlabs.trapperkeeper.core :refer [defservice]])) ;; A protocol that defines what functions our service will provide (defprotocol HelloService (hello [this]) (defservice hello-service HelloService ;; dependencies: none for this service [] ;; optional lifecycle functions that we can implement if we choose (init [this context] (println "Hello service initializing!") context) ;; implement our protocol functions (hello [this] (println "Hello there!"))) (defservice hello-consumer-service ;; no protocol required since this service doesn't export any functions. ;; express a dependency on the `hello` function from the `HelloService`. [[:HelloService hello]] (init [this context] (println "Hello consumer initializing; hello service says:") ;; call the function from the `hello-service`! (hello) context)) ``` Then, you need to define a Trapperkeeper bootstrap configuration file, which simply lists the services that you want to load at startup. This file should be named `bootstrap.cfg` and should be located at the root of your classpath (a good spot for it would be in your `resources` directory). ```clj hello/hello-consumer-service hello/hello-service ``` Lastly, set Trapperkeeper to be your `:main` in your Leiningen project file: ```clj :main puppetlabs.trapperkeeper.main ``` And now you should be able to run the app via `lein run --config ...`. This example doesn't do much; for a more interesting example that shows how you can use Trapperkeeper to create a web application, check out the [Example Web Service](https://github.com/puppetlabs/trapperkeeper-webserver-jetty9/tree/master/examples/ring_app) included in the Trapperkeeper webserver service project. To get started defining your own services in Trapperkeeper, head to the [Defining Services](Defining-Services.md) page. puppetlabs-trapperkeeper-d1f1135/examples/000077500000000000000000000000001463756611100207165ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/examples/java_service/000077500000000000000000000000001463756611100233575ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/examples/java_service/README.md000066400000000000000000000004471463756611100246430ustar00rootroot00000000000000Example: Building a Trapperkeeper service that wraps java code -------------------------------------------------------------- To run the example: lein trampoline run --config ./examples/java_service/config.conf \ --bootstrap-config ./examples/java_service/bootstrap.cfg puppetlabs-trapperkeeper-d1f1135/examples/java_service/bootstrap.cfg000066400000000000000000000001461463756611100260560ustar00rootroot00000000000000java-service-example.java-service/java-service java-service-example.java-service/java-service-consumerpuppetlabs-trapperkeeper-d1f1135/examples/java_service/config.conf000066400000000000000000000001471463756611100254750ustar00rootroot00000000000000global { # Points to a logback config file logging-config = examples/java_service/logback.xml }puppetlabs-trapperkeeper-d1f1135/examples/java_service/logback.xml000066400000000000000000000004521463756611100255040ustar00rootroot00000000000000 %d %-5p [%c{2}] %m%n puppetlabs-trapperkeeper-d1f1135/examples/java_service/src/000077500000000000000000000000001463756611100241465ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/examples/java_service/src/clj/000077500000000000000000000000001463756611100247165ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/examples/java_service/src/clj/java_service_example/000077500000000000000000000000001463756611100310725ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/examples/java_service/src/clj/java_service_example/java_service.clj000066400000000000000000000015471463756611100342340ustar00rootroot00000000000000(ns java-service-example.java-service (:import (java_service_example ServiceImpl)) (:require [puppetlabs.trapperkeeper.core :refer [defservice]] [clojure.tools.logging :as log])) (defprotocol JavaService (msg-fn [this]) (meaning-of-life-fn [this])) (defservice java-service JavaService [] ;; Service functions are implemented in a java `ServiceImpl` class (msg-fn [this] (ServiceImpl/getMessage)) (meaning-of-life-fn [this] (ServiceImpl/getMeaningOfLife))) (defservice java-service-consumer [[:JavaService msg-fn meaning-of-life-fn] [:ShutdownService request-shutdown]] (init [this context] (log/info "Java service consumer!") (log/infof "The message from Java is: '%s'" (msg-fn)) (log/infof "The meaning of life is: '%s'" (meaning-of-life-fn)) context) (start [this context] (request-shutdown) context)) puppetlabs-trapperkeeper-d1f1135/examples/java_service/src/java/000077500000000000000000000000001463756611100250675ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/examples/java_service/src/java/java_service_example/000077500000000000000000000000001463756611100312435ustar00rootroot00000000000000ServiceImpl.java000066400000000000000000000002741463756611100342540ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/examples/java_service/src/java/java_service_examplepackage java_service_example; public class ServiceImpl { public static String getMessage() { return "This came from java."; } public static int getMeaningOfLife() { return 42; } }puppetlabs-trapperkeeper-d1f1135/examples/shutdown_app/000077500000000000000000000000001463756611100234315ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/examples/shutdown_app/README.md000066400000000000000000000005151463756611100247110ustar00rootroot00000000000000This simple standalone application is for testing the shutdown functionality of Trapperkeeper. This is intended to be ran, and then killed with either Ctrl-C or the kill command, and the services with shutdown hooks should be called. You should see instructions upon starting the application. To run: lein test-external-shutdown puppetlabs-trapperkeeper-d1f1135/examples/shutdown_app/bootstrap.cfg000066400000000000000000000000721463756611100261260ustar00rootroot00000000000000examples.shutdown-app.test-external-shutdown/test-service puppetlabs-trapperkeeper-d1f1135/examples/shutdown_app/src/000077500000000000000000000000001463756611100242205ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/examples/shutdown_app/src/examples/000077500000000000000000000000001463756611100260365ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/examples/shutdown_app/src/examples/shutdown_app/000077500000000000000000000000001463756611100305515ustar00rootroot00000000000000test_external_shutdown.clj000066400000000000000000000011751463756611100360040ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/examples/shutdown_app/src/examples/shutdown_app(ns examples.shutdown-app.test-external-shutdown (:require [puppetlabs.trapperkeeper.core :as trapperkeeper] [puppetlabs.trapperkeeper.testutils.bootstrap :as testutils])) (trapperkeeper/defservice test-service [] (stop [this context] (println "If you see this printed out then shutdown works correctly!") context)) (defn -main [& args] (println "Waiting for a shutdown signal - use Ctrl-C or kill.") (println "You should see a message printed out when services are being shutdown.") (trapperkeeper/run {:config testutils/empty-config :bootstrap-config "examples/shutdown_app/bootstrap.cfg"})) puppetlabs-trapperkeeper-d1f1135/ext/000077500000000000000000000000001463756611100177005ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/ext/test/000077500000000000000000000000001463756611100206575ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/ext/test/custom-exit-behavior000077500000000000000000000011221463756611100246570ustar00rootroot00000000000000#!/usr/bin/env bash set -uexo pipefail usage() { echo "Usage: $(basename "$0")"; } misuse() { usage 1>&2; exit 2; } test $# -eq 0 || misuse tmpdir="$(mktemp -d "test-custom-exit-behavior-XXXXXX")" tmpdir="$(cd "$tmpdir" && pwd)" trap "$(printf 'rm -rf %q' "$tmpdir")" EXIT rc=0 ./tk -cp "$(pwd)/test" -- \ -d -b <(echo puppetlabs.trapperkeeper.custom-exit-behavior-test/custom-exit-behavior-test-service) \ 1>"$tmpdir/out" 2>"$tmpdir/err" || rc=$? cat "$tmpdir/out" "$tmpdir/err" test "$rc" -eq 7 grep -F 'Some excitement!' "$tmpdir/out" grep -F 'More excitement!' "$tmpdir/err" puppetlabs-trapperkeeper-d1f1135/ext/test/run-all000077500000000000000000000003661463756611100221640ustar00rootroot00000000000000#!/usr/bin/env bash set -uexo pipefail usage() { echo "Usage: [TRAPPERKEEPER_JAR=JAR] $(basename "$0")"; } misuse() { usage 1>&2; exit 2; } test $# -eq 0 || misuse ext/test/top-level-cli ext/test/custom-exit-behavior ext/test/signal-handling puppetlabs-trapperkeeper-d1f1135/ext/test/signal-handling000077500000000000000000000024301463756611100236430ustar00rootroot00000000000000#!/usr/bin/env bash set -uexo pipefail usage() { echo "Usage: $(basename "$0")"; } misuse() { usage 1>&2; exit 2; } await-file() ( local target="$1" set +x while ! test -e "$target"; do sleep 0.1; done ) tk_pid='' tmpdir='' on-exit() { if test "$tk_pid"; then kill "$tk_pid" status=0 wait "$tk_pid" || status=$? set +x echo tk exited with status "$status (143 is likely)" 1>&2 set -x fi rm -rf "$tmpdir" } trap on-exit EXIT test $# -eq 0 || misuse tmpdir="$(mktemp -d "test-signal-handling-XXXXXX")" tmpdir="$(cd "$tmpdir" && pwd)" # Start the test server, which repeatedly writes to the configured # target file, and make sure the target changes after a config file # change and signal. target_1="$tmpdir/target-1" target_2="$tmpdir/target-2" cat > "$tmpdir/config.json" < "$tmpdir/config.json" <&2; exit 2; } test $# -eq 0 || misuse tmpdir="$(mktemp -d "test-top-level-cli-XXXXXX")" tmpdir="$(cd "$tmpdir" && pwd)" trap "$(printf 'rm -rf %q' "$tmpdir")" EXIT ## Test handling an unknown option rc=0 ./tk -- --invalid-option 1>"$tmpdir/out" 2>"$tmpdir/err" || rc=$? cat "$tmpdir/out" "$tmpdir/err" test "$rc" -eq 1 grep -F 'Unknown option: "--invalid-option"' "$tmpdir/err" ## Test --help rc=0 ./tk -- --help 1>"$tmpdir/out" 2>"$tmpdir/err" || rc=$? cat "$tmpdir/out" "$tmpdir/err" test "$rc" -eq 0 grep -F 'Path to bootstrap config file' "$tmpdir/out" test $(grep -c -F 'Path to bootstrap config file' "$tmpdir/out") -eq 1 test $(wc -c < "$tmpdir/out") -eq 650 ## Test handling a missing bootstrap file rc=0 ./tk -- frobnicate ... 1>"$tmpdir/out" 2>"$tmpdir/err" || rc=$? cat "$tmpdir/out" "$tmpdir/err" test "$rc" -eq 1 grep -F 'Unable to find bootstrap.cfg file via --bootstrap-config' "$tmpdir/err" puppetlabs-trapperkeeper-d1f1135/ext/travisci/000077500000000000000000000000001463756611100215245ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/ext/travisci/prep-macos000077500000000000000000000004131463756611100235160ustar00rootroot00000000000000#!/usr/bin/env bash set -exu java -version # Something was wrong with travis' macos lein, so just grab our own mkdir -p ext/travisci/bin curl -o lein 'https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein' chmod +x lein mv lein ext/travisci/bin/ puppetlabs-trapperkeeper-d1f1135/jenkins/000077500000000000000000000000001463756611100205415ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/jenkins/deploy.sh000077500000000000000000000003541463756611100223760ustar00rootroot00000000000000#!/usr/bin/env bash set -e set -x git fetch --tags lein test echo "Tests passed!" lein release echo "Release plugin successful, pushing changes to git" git push origin --tags HEAD:$TRAPPERKEEPER_BRANCH echo "git push successful." puppetlabs-trapperkeeper-d1f1135/locales/000077500000000000000000000000001463756611100205225ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/locales/eo.po000066400000000000000000000243011463756611100214650ustar00rootroot00000000000000# Esperanto translations for puppetlabs.trapperkeeper package. # Copyright (C) 2017 Puppet # This file is distributed under the same license as the puppetlabs.trapperkeeper package. # Automatically generated, 2017. # msgid "" msgstr "" "Project-Id-Version: puppetlabs.trapperkeeper \n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: \n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "" "Invalid line in bootstrap config file:nnt{0}nnAll lines must be of the form: " "''/''." msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Loading bootstrap configs:n{0}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Loading bootstrap config from current working directory: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Loading bootstrap config from classpath: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "" "Unable to find bootstrap.cfg file via --bootstrap-config command line " "argument, current working directory, or on classpath" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Specified bootstrap config file does not exist: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "{0}:{1}n{2}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Duplicate implementations found for service protocol ''{0}'':n{1}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Unable to load service: {0}/{1}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Problem loading service ''{0}'' from {1}:{2}:n{3}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Unable to load service ''{0}'' from {1}:{2}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "" "Duplicate bootstrap entry found for service ''{0}'' on line number ''{1}'' " "in file ''{2}''" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "No entries found in any supplied bootstrap file(s):n{0}" msgstr "" #: src/puppetlabs/trapperkeeper/config.clj msgid "" "restart-file setting specified both on command-line and in config file, " "using command-line value: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/config.clj msgid "Configuration path ''{0}'' must exist and must be readable." msgstr "" #: src/puppetlabs/trapperkeeper/config.clj msgid "Duplicate configuration entry: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/core.clj msgid "Finished TK main lifecycle, shutting down Clojure agent threads." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Restart file {0} is not readable and/or writeable" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Number of restarts has exceeded Long/MAX_VALUE, resetting file to 1" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Restart file is unparseable, resetting file to 1" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Invalid service definition; expected a service definition (created via " "`service` or `defservice`); found: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Invalid service graph; service graphs must be nested maps of keywords to " "functions. Found: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Service ''{0}'' not found" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Service function ''{0}'' not found in service ''{1}''" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Services ''{0}'' not found" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Turns on debug mode" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Path to bootstrap config file" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Path to a configuration file or directory of configuration files, or a comma-" "separated list of such paths. See the documentation for a list of supported " "file types." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Path to directory plugin .jars" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Path to a file whose contents are incremented each time all of the " "configured services have been started." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Lifecycle function ''{0}'' for service ''{1}'' must return a context map " "(got: {2})" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Running lifecycle function ''{0}'' for service ''{1}''" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Error during service {0}!!!" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Initializing lifecycle worker loop." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Received shutdown command, shutting down services" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Clearing lifecycle worker channels for shutdown." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Shutdown in progress, ignoring message on shutdown channel: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Shutdown in progress, ignoring message on main lifecycle channel: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Service shutdown complete, exiting lifecycle worker loop" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Exception caught during shutdown!" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Lifecycle worker executing {0} lifecycle task." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Lifecycle worker completed {0} lifecycle task; awaiting next task." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Exception caught in lifecycle worker loop" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Unrecognized lifecycle task: %s" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "SIGHUP handler restarting TK apps." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Ignoring new SIGHUP restart requests; too many requests queued ({0})" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Registering SIGHUP handler for restarting TK apps" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "shutdown-on-error triggered because of exception!" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Beginning shutdown sequence" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Encountered error during shutdown sequence" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Putting shutdown message on shutdown channel." msgstr "" #. wait for the channel to send us the return value so we know it's done #: src/puppetlabs/trapperkeeper/internal.clj msgid "Waiting for response to shutdown message from lifecycle worker." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Finished shutdown sequence" msgstr "" #. else, the read from the channel returned a nil because it was closed, #. indicating that there was already a shutdown in progress, and thus the #. redundant shutdown request was ignored #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Response from lifecycle worker indicates shutdown already in progress, " "ignoring additional shutdown attempt." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Shutting down due to JVM shutdown hook." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Error occurred during shutdown" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Error during app buildup!" msgstr "" #: src/puppetlabs/trapperkeeper/logging.clj msgid "Uncaught exception" msgstr "" #: src/puppetlabs/trapperkeeper/logging.clj msgid "Debug logging enabled" msgstr "" #: src/puppetlabs/trapperkeeper/plugins.clj msgid "Class or namespace {0} found in both {1} and {2}" msgstr "" #: src/puppetlabs/trapperkeeper/plugins.clj msgid "Adding plugin .jar to classpath." msgstr "" #: src/puppetlabs/trapperkeeper/plugins.clj msgid "Plugins directory {0} does not exist" msgstr "" #: src/puppetlabs/trapperkeeper/services.clj msgid "Call to ''get-service'' failed; service ''{0}'' does not exist." msgstr "" #: src/puppetlabs/trapperkeeper/services/nrepl/nrepl_service.clj msgid "Starting nREPL service on {0} port {1}" msgstr "" #: src/puppetlabs/trapperkeeper/services/nrepl/nrepl_service.clj msgid "nREPL service disabled, not starting" msgstr "" #: src/puppetlabs/trapperkeeper/services/nrepl/nrepl_service.clj msgid "Shutting down nREPL service" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Invalid service definition; first form must be protocol or dependency list; " "found ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Invalid service definition; expected dependency list following protocol, " "found: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Invalid service definition; expected function definitions following " "dependency list, invalid value: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "Unrecognized service protocol ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "Specified service protocol ''{0}'' does not appear to be a protocol!" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Service protocol ''{0}'' includes function named ''{1}'', which conflicts " "with lifecycle function by same name" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Service attempts to define function ''{0}'', but does not provide protocol" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Service attempts to define function ''{0}'', which does not exist in " "protocol ''{1}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Service does not define function ''{0}'', which is required by protocol " "''{1}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Incorrect macro usage: service functions must be defined the same as a call " "to `reify`, eg: `(my-service-fn [this other-args] ...)`" msgstr "" puppetlabs-trapperkeeper-d1f1135/locales/trapperkeeper.pot000066400000000000000000000257661463756611100241370ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Puppet # This file is distributed under the same license as the puppetlabs.trapperkeeper package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: puppetlabs.trapperkeeper \n" "X-Git-Ref: 0debf327ca7e955e6db6e8c8e153cd47a80c6320\n" "Report-Msgid-Bugs-To: docs@puppet.com\n" "POT-Creation-Date: \n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "" "Invalid line in bootstrap config file:nnt{0}nnAll lines must be of the form: " "''/''." msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Loading bootstrap configs:n{0}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Loading bootstrap config from current working directory: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Loading bootstrap config from classpath: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "" "Unable to find bootstrap.cfg file via --bootstrap-config command line " "argument, current working directory, or on classpath" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Specified bootstrap config file does not exist: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "{0}:{1}n{2}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Duplicate implementations found for service protocol ''{0}'':n{1}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Unable to load service: {0}/{1}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Problem loading service ''{0}'' from {1}:{2}:n{3}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "Unable to load service ''{0}'' from {1}:{2}" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "" "Duplicate bootstrap entry found for service ''{0}'' on line number ''{1}'' " "in file ''{2}''" msgstr "" #: src/puppetlabs/trapperkeeper/bootstrap.clj msgid "No entries found in any supplied bootstrap file(s):n{0}" msgstr "" #: src/puppetlabs/trapperkeeper/config.clj msgid "Config file {0} must end in .conf or other recognized extension" msgstr "" #: src/puppetlabs/trapperkeeper/config.clj msgid "" "restart-file setting specified both on command-line and in config file, " "using command-line value: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/config.clj msgid "Configuration path ''{0}'' must exist and must be readable." msgstr "" #: src/puppetlabs/trapperkeeper/config.clj msgid "Duplicate configuration entry: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/core.clj msgid "Process exit requested" msgstr "" #: src/puppetlabs/trapperkeeper/core.clj msgid "Invalid program arguments" msgstr "" #: src/puppetlabs/trapperkeeper/core.clj msgid "Command line --help requested" msgstr "" #: src/puppetlabs/trapperkeeper/core.clj msgid "Malformed exit message: {0}n" msgstr "" #: src/puppetlabs/trapperkeeper/core.clj msgid "Invalid exit status requested, exiting with 2" msgstr "" #: src/puppetlabs/trapperkeeper/core.clj msgid "Finished TK main lifecycle, shutting down Clojure agent threads." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Restart file {0} is not readable and/or writeable" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Number of restarts has exceeded Long/MAX_VALUE, resetting file to 1" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Restart file is unparseable, resetting file to 1" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Invalid service definition; expected a service definition (created via " "`service` or `defservice`); found: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Invalid service graph; service graphs must be nested maps of keywords to " "functions. Found: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Service ''{0}'' not found" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Service function ''{0}'' not found in service ''{1}''" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Services ''{0}'' not found" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Turns on debug mode" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Path to bootstrap config file" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Path to a configuration file or directory of configuration files, or a comma-" "separated list of such paths. See the documentation for a list of supported " "file types." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Path to directory plugin .jars" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Path to a file whose contents are incremented each time all of the " "configured services have been started." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Lifecycle function ''{0}'' for service ''{1}'' must return a context map " "(got: {2})" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Running lifecycle function ''{0}'' for service ''{1}''" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Finished running lifecycle function ''{0}'' for service ''{1}''" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Error during service {0}!!!" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Initializing lifecycle worker loop." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Received shutdown command, shutting down services" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Clearing lifecycle worker channels for shutdown." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Shutdown in progress, ignoring message on shutdown channel: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Shutdown in progress, ignoring message on main lifecycle channel: {0}" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Service shutdown complete, exiting lifecycle worker loop" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Exception caught during shutdown!" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Lifecycle worker executing {0} lifecycle task." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Lifecycle worker completed {0} lifecycle task; awaiting next task." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Exception caught in lifecycle worker loop" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Unrecognized lifecycle task: %s" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "SIGHUP handler restarting TK apps." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Ignoring new SIGHUP restart requests; too many requests queued ({0})" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Registering SIGHUP handler for restarting TK apps" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "shutdown-on-error triggered because of exception!" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Beginning shutdown sequence" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Encountered error during shutdown sequence" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Putting shutdown message on shutdown channel." msgstr "" #. wait for the channel to send us the return value so we know it's done #: src/puppetlabs/trapperkeeper/internal.clj msgid "Waiting for response to shutdown message from lifecycle worker." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Finished shutdown sequence" msgstr "" #. else, the read from the channel returned a nil because it was closed, #. indicating that there was already a shutdown in progress, and thus the #. redundant shutdown request was ignored #: src/puppetlabs/trapperkeeper/internal.clj msgid "" "Response from lifecycle worker indicates shutdown already in progress, " "ignoring additional shutdown attempt." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Shutting down due to JVM shutdown hook." msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Error occurred during shutdown" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Error during app shutdown!" msgstr "" #: src/puppetlabs/trapperkeeper/internal.clj msgid "Error during app buildup!" msgstr "" #: src/puppetlabs/trapperkeeper/logging.clj msgid "Uncaught exception" msgstr "" #: src/puppetlabs/trapperkeeper/logging.clj msgid "Debug logging enabled" msgstr "" #: src/puppetlabs/trapperkeeper/plugins.clj msgid "Class or namespace {0} found in both {1} and {2}" msgstr "" #: src/puppetlabs/trapperkeeper/plugins.clj msgid "Adding plugin {0} to classpath." msgstr "" #: src/puppetlabs/trapperkeeper/plugins.clj msgid "Plugins directory {0} does not exist" msgstr "" #: src/puppetlabs/trapperkeeper/services.clj msgid "Call to ''get-service'' failed; service ''{0}'' does not exist." msgstr "" #: src/puppetlabs/trapperkeeper/services/nrepl/nrepl_service.clj msgid "Starting nREPL service on {0} port {1}" msgstr "" #: src/puppetlabs/trapperkeeper/services/nrepl/nrepl_service.clj msgid "nREPL service disabled, not starting" msgstr "" #: src/puppetlabs/trapperkeeper/services/nrepl/nrepl_service.clj msgid "Shutting down nREPL service" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Invalid service definition; first form must be protocol or dependency list; " "found ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Invalid service definition; expected dependency list following protocol, " "found: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Invalid service definition; expected function definitions following " "dependency list, invalid value: ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "Unrecognized service protocol ''{0}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "Specified service protocol ''{0}'' does not appear to be a protocol!" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Service protocol ''{0}'' includes function named ''{1}'', which conflicts " "with lifecycle function by same name" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Service attempts to define function ''{0}'', but does not provide protocol" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Service attempts to define function ''{0}'', which does not exist in " "protocol ''{1}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Service does not define function ''{0}'', which is required by protocol " "''{1}''" msgstr "" #: src/puppetlabs/trapperkeeper/services_internal.clj msgid "" "Incorrect macro usage: service functions must be defined the same as a call " "to `reify`, eg: `(my-service-fn [this other-args] ...)`" msgstr "" puppetlabs-trapperkeeper-d1f1135/plugin-test-resources/000077500000000000000000000000001463756611100233635ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/plugin-test-resources/bad-plugins/000077500000000000000000000000001463756611100255705ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/plugin-test-resources/bad-plugins/kitchensink-0.1.0.jar000066400000000000000000000316701463756611100312410ustar00rootroot00000000000000PKJlCMETA-INF/MANIFEST.MFMLK-. K-*ϳR03r*)uRH.(LNr.JM,IMfe楧)rPKwEHPKJlC-META-INF/maven/puppetlabs/kitchensink/pom.xmlWmO0ίyBA!lM4i_M:vd;KRRXa>zw=w>g- ~6 lǧlpGKu`\ Zp o_ljkvivWj zr=q SYVGs)bM@ޭb757Ls![_)DfF G{q^*ۓ1 tQ#z;1}&ϟftd]=TH&p/]i ԀvhoP"{#Y?+z?\T]]:vͱwyT֏_ c~foړr7ߘPKJkPKJlC4META-INF/maven/puppetlabs/kitchensink/pom.propertiesK н:i (Rh.0I:1t^u'6 /H R؀|f J7P:uDM7(XmvY]\-FlO)0e^qɿ9/o ']|PK ^UPKJlC5META-INF/leiningen/puppetlabs/kitchensink/project.clj}M0BRn=B%˒#m9$4͚T43^ͼRBkA@أ
CM.^h #ܡ('x=YK=HRduPKoJE^PKJlC project.clj}M0BRn=B%˒#m9$4͚T43^ͼRBkA@أ
CM.^h #ܡ('x=YK=HRduPKoJE^PKvqkCpuppetlabs/kitchensink/ssl.cljWmkF_15 V@kzPZU|ޙ]YB w;o33 e]baGeӵ,*ch6,DI$FuKw#T7zVңzZ]I"1jk>}mI*?~QK}]t cs49\CLV/&#LJ?)O?c6\?ԕLֹIrZb3a7{0Erm_~2a#%8 BY%rYf'Q\dYp1erYJ9B_QܔAUL7rC|ŃLm2A; 04o cEJ{;qpGȠPy/fz I `r S5ف+(<  A}=E3Fm^ue^2*iJ]dD6{8wѣ @.-ܡ`# MVWjM55'#Ǯ",L.DK]m&!j QS]o\ǘs&!gz?PF)ѧ?RcAEApi|/aZ'K7ͳ)vr;_F!P^h?d_"h9k\ N^WasXH$DA+i)ğ8pKa`3\8_n]dndJcsy<ןRYZmןJA S3a[^;_N%x!4 2 ɓ.uyd:3  op9\z f`jStJ&*O;3V O&;%2)!Q4"7!߁xRmQv0!ݞ8!6v4.|֬=-xCbEW#\ [ 0صQ;ՒK_t6e$uuCXރG{N(]j*&6&4RL7S >a̷}8B!bvl}U Dw0{'븶gp?9 w:-w/؈DaȣNCPK璕EPKvqkC$puppetlabs/kitchensink/testutils.cljen0 <ǘvCHu3RK *-Bbφ|&(ylDI2zӴ!R<% ̤Cp*(vf+I달*^%.84Ϟ̨>0xѰK *B'KJ*~.}D -g+B=u"mWwkvv٠9Kw6.ȕnpJLz |Z%/v~rIt|GBc }!pxvE;0Rدe>֝hsPK95;PKvqkC$puppetlabs/kitchensink/classpath.cljT]o0 | :lp?}Ȇi螂ad:VH%5ٵ7[xt#t(9\jciG7qc(n X+O ~|ӊ5N&C%l`f\Xgjh䤑Yc] 668KUQSa\.ke ef'_<@@ ZL`yz2)pRf/0[NXb Hk+x+4;1믛N8=8#?ZBLRl}L M#x?[YcsdyˉHQd9rGh!#Βv2&ObZh 88֚lar¢(7L>c;<ZYNq} A0LM>)mmv[Ӣ_)޶eD9^Ui A+38di0&RdO5nlger*Wy+m"[R’Iҥ@׀0xwJ[o\ltddoSչG1AkJ^j TK1[-ϓjsS-? ~ E,:x-ؓC~̔w}y7eL.DOlh'5Ne/Y,I 3oր?N-vuRMs f#7-)k\8qhRdS\PKPKFlCpuppetlabs/kitchensink/core.clj\ms7_ЕP{v%x/"gsw6g@rya3ۯnj\MF߻OգGjzmm6̫ӧ1mSe5ͮ]JZ("Slz0nVߚ5iTKc7:5)z fUF7^֚/Vm׵Zkڼ4jI0ڵn_+6fcB/Fvbi#Ӵn̑RY^nUW4?Q/^?WoizEh_OFc~rUZoL[cU]vZԫ(6#cmAwaEC߽qY2xҺt6=k5bƍљi&=OAY\ TQ]n[2n=B̡yquC1 XGɉ~g*3w-^=aRB#<uX;~)N]ؗBH4Pw~")Fڱ©Z-dV92/ $V0Ҟc[ @'֤o]{~j:̪e3 tUY~[҂HnV]I1u؁h2'X+9bC!y%6}=$3U_{{>C[5M¿↹o{+L"5yUc; NM Y{$UU^qSV[W̩z),HJmؼ=}3&JnK{#.L5Riqъ GNH,j{P<_LרSx+FT<DKct`#NbL bCJUDǩ>sˀaENLvOD9a.+ r}5>&aW0HޯLي"kaڭ!2zllLsV{ |5~7>gѱiK:Mk)նkÿ'cO@=pv ]4T7QکvuR:ߎK[zp c: |N-b){X&s;)0kG|1FD$$14ﯝ4E)Esգ3<ñ` /WCX跻8/6c/(d)co'fI[:K:0֑!`e_fxnM^zk2)}zcAo  ^h^Zt꼄^ll L$ǁwcv$/"+lͭw/II EOe(Y0zB7bu{үn)t& 4=H#1%F<y@?lÒaKlAn{ቚ۵2Uޕ0' |9!QjaQI҅z);SpAlG[$Xvc| osB~aTYg隥 ;WW7)0^)ZId-!% f׺fPe[s NyZ$'hqy ;_^)JR{H4 rzgkogOƚ6fra<&H.u)FBnLX=JusMfkA$qiI4VɄ>bFg$ha# |#%#ghNfB#&CxQctM\Gքbi-"`#^%Ar@,e'X}licX8lC vhbGòFNa,,KWw\;S'jOlqMئj^Oߑdof wHRc:''w']Z☗ӕ/H 5'1a'~YK4fSv21'f!p9˭$_&a8a&g5;,0uY9gk&x ^ _!F-=^g1lR|Ct6A l*.?z`m XHGSH}⌒?-BOq^ȍ MgvIbG*s׊iD,8_NK6H nASӅY(+#f"ߋ3vr^ֺH\y2p WE%ҾtֈލBŀIP#-)IgS?7ªi$8#>d(l b[5[ٚ-Vtx>byP0r`IVa4b^d1OjʞeScm M!ߗMLuB+jUlU,dsOu*vxmj*6Ķ3vIg90{@{'Op sq/wtgs6G{Mb#:A N`L~62:eEIg`p&{79n7krF>{Br>-QCCɗ hR4g{ږ4C$P*Aiһ!Xw{u}Vnȯ8SHq~`zvlM,xUz;t;_7HW1#9Od"~ZwqYK6J3,;; X6,w$#[ RB7’y3c#ė- \% 64x3ɲ@6m-q'7H 5cb֙+!pl2xNU>J|PmDQB4>8rnŝ N/op(&or{KQс /ɒ|:  k[չ<ѥOBd+(aȑLbzL46;ĩ0K lkb^#|bM~C\i`TZy&LehTwP݀3ۚԿLJmW\{ ڢfTm,V$%:!<::` O<}ƓlH>5EgO `C Ggn9Tc_*јdx?" G ]c%eKbUX58گGw{|6{OO>_2pBOW'UiGn>ߡ3 (^7W5un$ꎤ>|M&Cb7U2А^XsYMCE4݆f^%N Q=ͮ"WHPqA`e" ܴ޲c5%Fz*-u[%&Px63d/R-Е'< DSKs4FG#ϓ9!9PR"AO_cs8eybQ=;0.j:&sK()?2i 5GZ1DVΆ&-ءmM]5&+fIOSmx%ԅ%IP~y-8FDؙ]@㽶J ~0߾ӯi`?z\d,$9"(pY'tɑ^v@}?eO}QGWFp)o-Jo)&B8QhwGZ> iv˵0x7QO@nx(G+]Vo>jIӒ1UT]3 Ywh;o4w;f,!rr" G\ymr9[Iru^.kHk':q¬m.]]NTe O/YqiN\B#vG],#jntn. gW(2 mG3hUԋuYby{y)n+2Cc7mPdҧB=YcNzSuAv'bį{^H>M&"ܣ;3mΔtH@LOKI-e^Oф6!F/0(t'L_:a}yۨ^dFMdCz\ǡ< -@:1{~?C kR@G {iJ^a-VYuf($k2>2s/^xJS oJ)*̗^H.?Nt$P˹ =Dl$R~uYc8bLɭGMN\btE1WVz8=sqgA#wqJCb`DKnteZN,2S4 \̔I]sUppyAiW,%qOOrS [ I~yHq Oɥ+p@_qZꆅ%CO0 rƭ/e8DոU7 _ݹܑT[kb"Kz*CɍD}TFs%@>xظCGjwR2nUDƫ7*]|KKL/?^}`;<9E.>+}({ _W! xCbb ѵ(fXvHhYU72!ʇZpƁ"\zB7SZf}r#Gsϣh7.h`NIɢ:t/|(}2uNg^>,H֏'0m4| SqSl"%ƌγ]h"&9{ǟg㩺D%bsaQ(Fಓcq\zj= y2ZY >p"/)LP BOGˎ{|UJ?ٵ ɶjfЧtFK$]!E߀>+pE5y=DjA: `*Z#I\p/4 G%fs_tMy}֚봜*̒Q-"lxr;c@28~E3D“hhC'P0)F|XVp[&w/\W܊ݟ-fCYr:Js?@y^ٺmy($L|:2A]s.>?k}vnӼ ^:~|ug2)1Ht59"@?FosJ8nⳑkR-ITp*1BEQI-uIW/ i=v!o^| r$sMp >3N)8AʡCor+RReLܖIxɀu@Hh g>!pPme__ j\.2[ k*#]\pK]ˮ]Ghvsy52-78_˚S4Lr?>y2]xoZ-j,=dft1cH_=.%?l-(݋\{_GVҷ z?-R[Ƥ?!Di\H*~D\1=O;5bkÝiʷqAP}GQiqsW‰ ` tBI|= ][׉Wɰ8(IK+ri+Q_N p[!+A4!K[@xoҢ=R dw`:Ii 6%[ 7.%06 Jt11&`'BEy7qB c 10,</`)JE}3 2qV2* H%'N!Ι/_3T\,U"LmDzMӭ0ܺ[Ik72mgמ,aLeweO שOLC5~Pޯ&t\`>EWSweIFc-"PK~,xMPKJlCwEHMETA-INF/MANIFEST.MFPKJlCJk-META-INF/maven/puppetlabs/kitchensink/pom.xmlPKJlC ^U4mMETA-INF/maven/puppetlabs/kitchensink/pom.propertiesPKJlCoJE^5RMETA-INF/leiningen/puppetlabs/kitchensink/project.cljPKJlCoJE^ Wproject.cljPKvqkC璕E2 puppetlabs/kitchensink/ssl.cljPKvqkC95;$ppuppetlabs/kitchensink/testutils.cljPKvqkC$puppetlabs/kitchensink/classpath.cljPKFlC~,xMpuppetlabs/kitchensink/core.cljPK 0puppetlabs-trapperkeeper-d1f1135/plugin-test-resources/plugins/000077500000000000000000000000001463756611100250445ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/plugin-test-resources/plugins/test-service.jar000066400000000000000000000012761463756611100301650ustar00rootroot00000000000000PK M2Dtest_services/UT RRux PKM2D@&test_services/plugin_test_services.cljUT RRux RN0 +v$ݷGU $8)|=N֍"8j\m6׍|PoۇtneT{eZBkFwtFHخmZTI*V|bd%YaCClvjqaX^EmL ?qbA3yB89:~\&J B(VV}eln<;q(N/'. Returns a 2-item vector containing the namespace and the service name. Throws an IllegalArgumentException if the line is not valid." [line :- schema/Str] (if-let [[_match namespace service-name] (re-matches #"^([a-zA-Z0-9\.\-]+)/([a-zA-Z0-9\.\-]+)$" line)] {:namespace namespace :service-name service-name} (throw+ {:type ::bootstrap-parse-error :message (i18n/trs "Invalid line in bootstrap config file:\n\n\t{0}\n\nAll lines must be of the form: ''/''." line)}))) (schema/defn ^:private remove-comments :- schema/Str "Given a line of text from the bootstrap config file, remove anything that is commented out with either a '#' or ';'. If the entire line is commented out, an empty string is returned." [line :- schema/Str] (-> line (string/replace #"(?:#|;).*$" "") (string/trim))) (schema/defn find-bootstraps-from-path :- [schema/Str] "Given a path, return a list of .cfg files found there. - If the path leads directly to a file, return a list with a single item. - If the path leads to a directory, return a list of any .cfg files found there. - If the path doesn't lead to a file or directory, attempt to load a file from a URI (for files in jars)" [config-path :- schema/Str] (if (fs/directory? config-path) (map str (fs/glob (fs/file config-path "*.cfg"))) [config-path])) (schema/defn ^:private config-from-cli :- [schema/Str] "Given the data from the command-line (parsed via `core/parse-cli-args!`), check to see if the caller explicitly specified the location of one or more bootstrap config files. If so, return an object that can be read via `reader` (will normally be a `file`, but in the case of a config file inside of a .jar, it will be an `input-stream`). Throws an IllegalArgumentException if a location was specified but the file doesn't actually exist." [cli-data :- common/CLIData] (when (contains? cli-data :bootstrap-config) (when-let [config-path (cli-data :bootstrap-config)] (let [config-files (flatten (map find-bootstraps-from-path (string/split config-path #",")))] (log/debug (i18n/trs "Loading bootstrap configs:\n{0}" (string/join "\n" config-files))) config-files)))) (schema/defn ^:private config-from-cwd :- [schema/Str] "Check to see if there is a bootstrap config file in the current working directory; if so, return it." [] (let [config-file (-> bootstrap-config-file-name (io/file) (.getAbsoluteFile))] (when (.exists config-file) (let [config-file-path (.getAbsolutePath config-file)] (log/debug (i18n/trs "Loading bootstrap config from current working directory: ''{0}''" config-file-path)) [config-file-path])))) (defn- resource [name] ;; Fixes io/resource crashes when context class loader is nil. ;; https://clojure.atlassian.net/browse/CLJ-2431 (.getResource (or (.getContextClassLoader (Thread/currentThread)) (ClassLoader/getSystemClassLoader)) name)) (schema/defn config-from-classpath :- [(schema/maybe schema/Str)] "Check to see if there is a bootstrap config file available on the classpath; if so, return it." [] (when-let [classpath-config (resource bootstrap-config-file-name)] (log/debug (i18n/trs "Loading bootstrap config from classpath: ''{0}''" classpath-config)) [(str classpath-config)])) (schema/defn find-bootstrap-configs :- [schema/Str] "Get the bootstrap config files from: 1. the file path specified on the command line, or 2. the current working directory, or 3. the classpath Throws an exception if the file cannot be found." [cli-data :- common/CLIData] (if-let [bootstrap-configs (or (config-from-cli cli-data) (config-from-cwd) (config-from-classpath))] bootstrap-configs (throw (IllegalStateException. (i18n/trs "Unable to find bootstrap.cfg file via --bootstrap-config command line argument, current working directory, or on classpath"))))) (schema/defn indexed "Returns seq of [index, item] pairs [:a :b :c] -> ([0 :a] [1 :b] [2 :c])" [coll] (map vector (range) coll)) (schema/defn wrap-uri-error [config-path :- schema/Str cause :- Throwable] (IllegalArgumentException. (i18n/trs "Specified bootstrap config file does not exist: ''{0}''" config-path) cause)) (schema/defn read-config :- [schema/Str] "Opens a bootstrap file from either a path or URI, and returns each line from the config. Throws an exception if there is a problem reading the file or the URI can't be loaded" [config-path :- schema/Str] (if (fs/readable? config-path) (line-seq (io/reader (fs/file config-path))) (try ; If it's not a file, attempt to read it as a URI ; TODO: TK-363 - Check to make sure the URI points to a jar file (line-seq (io/reader (URI. config-path))) ; Thrown by URI constructor (catch URISyntaxException e (throw (wrap-uri-error config-path e))) ; Thrown by reader URI.toURL() when URI is not absolute (catch IllegalArgumentException e (throw (wrap-uri-error config-path e))) ; Thrown if a valid URI points to a file that does not exist (catch FileNotFoundException e (throw (wrap-uri-error config-path e)))))) (schema/defn get-annotated-bootstrap-entries :- [AnnotatedBootstrapEntry] "Reads each bootstrap entry into a map with the line number and file the entry is from. Returns a list of maps." [configs :- [schema/Str]] (for [config configs [line-number line-text] (indexed (map remove-comments (read-config config))) :when (seq line-text)] {:bootstrap-file config :line-number (inc line-number) :entry line-text})) (defn find-duplicates "Collects duplicates base on running f on each item in coll. Returns a map where the keys will be the result of running f on each item, and the values will be lists of items that are duplicates of eachother" [coll f] (->> coll (group-by f) ; filter out map values with only 1 item (remove #(= 1 (count (val %)))) (into {}))) (schema/defn duplicate-protocol-error :- IllegalArgumentException "Returns an IllegalArgumentException describing what services implement the same protocol, including the line number and file the bootstrap entries were found" [duplicate-services :- {schema/Keyword [(schema/protocol services/ServiceDefinition)]} service->entry-map :- {(schema/protocol services/ServiceDefinition) AnnotatedBootstrapEntry}] (let [make-error-message (fn [service] (let [entry (get service->entry-map service)] (i18n/trs "{0}:{1}\n{2}" (:bootstrap-file entry) (:line-number entry) (:entry entry)))) error-messages (for [[protocol-id services] duplicate-services] (i18n/trs "Duplicate implementations found for service protocol ''{0}'':\n{1}" protocol-id (string/join "\n" (map make-error-message services))))] (IllegalArgumentException. (string/join "\n" error-messages)))) (schema/defn check-duplicate-service-implementations! "Throws an exception if two services implement the same service protocol" [services :- [(schema/protocol services/ServiceDefinition)] bootstrap-entries :- [AnnotatedBootstrapEntry]] ; Zip up the services and bootstrap entries and construct a map out of them ; to use as a lookup table below (let [service->entry-map (zipmap services bootstrap-entries) ; Find duplicates base on the service id returned by calling service-def-id ; on each service duplicates (find-duplicates services services/service-def-id)] (when (seq duplicates) (throw (duplicate-protocol-error duplicates service->entry-map))))) (schema/defn ^:private resolve-service! :- (schema/protocol services/ServiceDefinition) "Given the namespace and name of a service, loads the namespace, calls the function, validates that the result is a valid service definition, and returns the service definition. Throws an `IllegalArgumentException` if the service definition cannot be resolved." [resolve-ns :- schema/Str service-name :- schema/Str] (try (require (symbol resolve-ns)) (catch FileNotFoundException e (throw+ {:type ::missing-service :message (i18n/trs "Unable to load service: {0}/{1}" resolve-ns service-name) :cause e}))) (if-let [service-def (ns-resolve (symbol resolve-ns) (symbol service-name))] (internal/validate-service-graph! (var-get service-def)) (throw+ {:type ::missing-service :message (i18n/trs "Unable to load service: {0}/{1}" resolve-ns service-name)}))) (schema/defn bootstrap-error :- IllegalArgumentException "Returns an IllegalArgumentException meant to wrap other errors relating to bootstrap problems. Includes the file and line number at which each problematic service entry was found" [entry :- schema/Str bootstrap-file :- schema/Str line-number :- schema/Int original-message :- schema/Str] (IllegalArgumentException. (i18n/trs "Problem loading service ''{0}'' from {1}:{2}:\n{3}" entry bootstrap-file line-number original-message))) (schema/defn resolve-and-handle-errors! :- (schema/maybe (schema/protocol services/ServiceDefinition)) "Attempts to resolve a bootstrap entry into a ServiceDefinition. If the bootstrap entry can't be resolved, logs a warning and returns nil. Throws an IllegalArgumentException if there is a problem parsing the bootstrap entry, or if the service is found but it has an invalid service graph." [{:keys [bootstrap-file line-number entry]} :- AnnotatedBootstrapEntry] (try+ (let [{:keys [namespace service-name]} (parse-bootstrap-line! entry)] (resolve-service! namespace service-name)) (catch [:type ::missing-service] {:keys [_message]} (log/warn (i18n/trs "Unable to load service ''{0}'' from {1}:{2}" entry bootstrap-file line-number))) ; Catch and re-throw as java exception (catch [:type ::internal/invalid-service-graph] {:keys [message]} (throw (bootstrap-error entry bootstrap-file line-number message))) (catch [:type ::bootstrap-parse-error] {:keys [message]} (throw (bootstrap-error entry bootstrap-file line-number message))))) (schema/defn resolve-services! :- [(schema/protocol services/ServiceDefinition)] "Resolves each bootstrap entry into an instance of a trapperkeeper ServiceDefinition. Logs a warning if the bootstrap entry can't be resolved. Throws an IllegalArgumentException if there is a problem parsing the bootstrap entry, or if the service is found but it has an invalid service graph." [bootstrap-entries :- [AnnotatedBootstrapEntry]] (remove nil? (map resolve-and-handle-errors! bootstrap-entries))) (schema/defn remove-duplicate-entries :- [AnnotatedBootstrapEntry] "Removes any duplicate entries from the list of AnnotatedBootstrapEntry maps. A entry is considered a duplicate if the :entry key is the same between two entries. Logs warnings for each duplicate found." [entries :- [AnnotatedBootstrapEntry]] (let [into-ordered-list ; Accumulates bootstrap entries into :set to test for duplicates, and ; accumulates the AnnotatedBootstrapEntry's into :ordered-entries so ; that the order of their line numbers is maintained (fn [acc annotated-entry] (if (contains? (:set acc) (:entry annotated-entry)) (do (log/warn (i18n/trs "Duplicate bootstrap entry found for service ''{0}'' on line number ''{1}'' in file ''{2}''" (:entry annotated-entry) (:line-number annotated-entry) (:bootstrap-file annotated-entry))) acc) {:set (conj (:set acc) (:entry annotated-entry)) :ordered-entries (conj (:ordered-entries acc) annotated-entry)}))] ; Reduce the bootstrap entries into an ordered list of unique entries and return it (:ordered-entries (reduce into-ordered-list {:set #{} :ordered-entries []} entries)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Public ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (schema/defn parse-bootstrap-configs! :- [(schema/protocol services/ServiceDefinition)] "Parse multiple trapperkeeper bootstrap configuration files and return the service graph that is the result of merging the graphs of all of the services specified in the configuration files." [configs :- [schema/Str]] ; We remove the duplicate entries (the exact same namespace and name) to allow ; the user to have duplicate entries in their bootstrap files. If we didn't ; remove them, it would look like two services were trying to implement the ; same protocol when we check for duplicate service implementations. We want ; to allow entries with the same exact name in order to support workflows ; where users are preparing to upgrade and might copy an entry to another file. (let [bootstrap-entries (remove-duplicate-entries (get-annotated-bootstrap-entries configs))] (when (empty? bootstrap-entries) (throw (Exception. (i18n/trs "No entries found in any supplied bootstrap file(s):\n{0}" (string/join "\n" configs))))) (let [resolved-services (resolve-services! bootstrap-entries)] (check-duplicate-service-implementations! resolved-services bootstrap-entries) resolved-services))) (schema/defn parse-bootstrap-config! :- [(schema/protocol services/ServiceDefinition)] "Parse a single bootstrap configuration file and return the service graph that is the result of merging the graphs of all the services specified in the configuration file" [config :- schema/Str] (parse-bootstrap-configs! [config])) puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/common.clj000066400000000000000000000012041463756611100266760ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.common (:require [schema.core :as schema])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Schemas ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def CLIData {(schema/optional-key :debug) schema/Bool (schema/optional-key :bootstrap-config) schema/Str (schema/optional-key :config) schema/Str (schema/optional-key :plugins) schema/Str (schema/optional-key :restart-file) schema/Str (schema/optional-key :help) schema/Bool}) puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/config.clj000066400000000000000000000127731463756611100266700ustar00rootroot00000000000000;;;; ;;;; This namespace contains trapperkeeper's built-in configuration service, ;;;; which is based on .ini config files. ;;;; ;;;; This service provides a function, `get-in-config`, which can be used to ;;;; retrieve the config data read from the ini files. For example, ;;;; given an .ini file with the following contents: ;;;; ;;;; [foo] ;;;; bar = baz ;;;; ;;;; The value of `(get-in-config [:foo :bar])` would be `"baz"`. ;;;; ;;;; Also provides a second function, `get-config`, which simply returns ;;;; the entire map of configuration data. ;;;; (ns puppetlabs.trapperkeeper.config (:import (java.io FileNotFoundException PushbackReader)) (:require [clojure.java.io :as io] [clojure.string :as str] [clojure.edn :as edn] [me.raynes.fs :as fs] [puppetlabs.kitchensink.core :as ks] [puppetlabs.config.typesafe :as typesafe] [puppetlabs.trapperkeeper.services :refer [service service-context]] [puppetlabs.trapperkeeper.logging :refer [configure-logging!]] [clojure.tools.logging :as log] [schema.core :as schema] [puppetlabs.trapperkeeper.common :as common] [puppetlabs.i18n.core :as i18n])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Service protocol (defprotocol ConfigService (get-config [this] "Returns a map containing all of the configuration values") (get-in-config [this ks] [this ks default] "Returns the individual configuration value from the nested configuration structure, where ks is a sequence of keys. Returns nil if the key is not present, or the default value if supplied.")) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Private (defn config-file->map [file] (condp (fn [vals ext] (contains? vals ext)) (fs/extension file) #{".ini"} (ks/ini-to-map file) #{".json" ".conf" ".properties"} (typesafe/config-file->map file) #{".edn"} (edn/read (PushbackReader. (io/reader file))) (throw (IllegalArgumentException. (i18n/trs "Config file {0} must end in .conf or other recognized extension" (-> file str pr-str)))))) (defn override-restart-file-from-cli-data [config-data cli-data] (if-let [cli-restart-file (:restart-file cli-data)] (do (when (get-in config-data [:global :restart-file]) (log/warnf (i18n/trs "restart-file setting specified both on command-line and in config file, using command-line value: ''{0}''" cli-restart-file))) (assoc-in config-data [:global :restart-file] cli-restart-file)) config-data)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Public (defn get-files-from-config "Given a path to a file or directory, return a list of all files contained that have valid extensions for a TK config." [path] (when-not (.canRead (io/file path)) (throw (FileNotFoundException. (i18n/trs "Configuration path ''{0}'' must exist and must be readable." path)))) (if-not (fs/directory? path) [path] (mapcat #(fs/glob (fs/file path %)) ["*.ini" "*.conf" "*.json" "*.properties" "*.edn"]))) (defn load-config "Given a path to a configuration file or directory of configuration files, or a string of multiple paths separated by comma, parse the config files and build up a trapperkeeper config map. Can be used to implement CLI tools that need access to trapperkeeper config data but don't need to boot the full TK framework." [paths] (let [files (flatten (map get-files-from-config (str/split paths #",")))] (->> files (map ks/absolute-path) (map config-file->map) (apply ks/deep-merge-with-keys (fn [ks & _] (throw (IllegalArgumentException. (i18n/trs "Duplicate configuration entry: {0}" ks))))) (merge {})))) (defn config-service "Returns trapperkeeper's configuration service. Expects to find a command-line argument value for `:config`; the value of this parameter should be the path to an .ini file or a directory of .ini files." [config-data-fn] (service ConfigService [] (init [this context] (assoc context :config (config-data-fn))) (get-config [this] (let [{:keys [config]} (service-context this)] config)) (get-in-config [this ks] (let [{:keys [config]} (service-context this)] (get-in config ks))) (get-in-config [this ks default] (let [{:keys [config]} (service-context this)] (get-in config ks default))))) (schema/defn parse-config-data :- (schema/pred map?) "Parses the .ini, .edn, .conf, .json, or .properties configuration file(s) and returns a map of configuration data. If no configuration file is explicitly specified, will act as if it was given an empty configuration file." [cli-data :- common/CLIData] (let [debug? (or (:debug cli-data) false)] (if-not (contains? cli-data :config) {:debug debug?} (-> (:config cli-data) (load-config) (assoc :debug debug?) (override-restart-file-from-cli-data cli-data))))) (defn initialize-logging! "Initializes the logging system based on the configuration data." [config-data] (let [debug? (get-in config-data [:debug]) log-config (get-in config-data [:global :logging-config])] (configure-logging! log-config debug?))) puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/core.clj000066400000000000000000000252171463756611100263500ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.core (:require [clojure.tools.logging :as log] [puppetlabs.kitchensink.core :refer [without-ns]] [puppetlabs.trapperkeeper.services :as services] [puppetlabs.trapperkeeper.app :as app] [puppetlabs.trapperkeeper.bootstrap :as bootstrap] [puppetlabs.trapperkeeper.internal :as internal] [puppetlabs.trapperkeeper.config :as config] [puppetlabs.trapperkeeper.plugins :as plugins] [schema.core :as schema] [puppetlabs.trapperkeeper.common :as common] [puppetlabs.i18n.core :as i18n]) (:import (clojure.lang ExceptionInfo))) (def #^{:macro true :doc "An alias for the `puppetlabs.trapperkeeper.services/service` macro so that it is accessible from the core namespace along with the rest of the API."} service #'services/service) (def #^{:macro true :doc "An alias for the `puppetlabs.trapperkeeper.services/defservice` macro so that it is accessible from the core namespace along with the rest of the API."} defservice #'services/defservice) (defn build-app "Given a list of services and a map of configuration data, build an instance of a TrapperkeeperApp. Services are not yet initialized or started. This function is mainly intended for use in a REPL, for developing using the 'reloaded' pattern. ( http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded ) Returns a TrapperkeeperApp instance. You may call the lifecycle functions (`init`, `start`, `stop`) as you see fit; if you'd like to have the trapperkeeper framework block the main thread to wait for a shutdown event, call `init`, `start`, and then `run-app`." [services config-data] {:pre [(sequential? services) (every? #(satisfies? services/ServiceDefinition %) services) (ifn? config-data)] :post [(satisfies? app/TrapperkeeperApp %)]} (let [config-data-fn (if (map? config-data) (constantly config-data) config-data)] (config/initialize-logging! (config-data-fn)) (internal/build-app* services config-data-fn))) (schema/defn boot-services-with-cli-data :- (schema/protocol app/TrapperkeeperApp) "Given a list of ServiceDefinitions and a map containing parsed cli data, create and boot a trapperkeeper app. This function can be used if you prefer to do your own CLI parsing and loading ServiceDefinitions; it circumvents the normal trapperkeeper `bootstrap.cfg` boot process, but still allows trapperkeeper to handle the parsing of your service configuration data. Returns a TrapperkeeperApp instance. Call `run-app` on it if you'd like to block the main thread to wait for a shutdown event." [services :- [(schema/protocol services/ServiceDefinition)] cli-data :- common/CLIData] (let [config-data-fn #(config/parse-config-data cli-data)] (config/initialize-logging! (config-data-fn)) (internal/boot-services* services config-data-fn))) (defn boot-services-with-config-fn "Given a list of ServiceDefinitions and a function that returns a map containing parsed cli data, create and boot a trapperkeeper app. This function can be used if you prefer to do your own CLI parsing and loading ServiceDefinitions; it circumvents the normal trapperkeeper `bootstrap.cfg` boot process, but still allows trapperkeeper to handle the parsing of your service configuration data. Returns a TrapperkeeperApp instance. Call `run-app` on it if you'd like to block the main thread to wait for a shutdown event." [services config-data-fn] {:pre [(sequential? services) (every? #(satisfies? services/ServiceDefinition %) services) (ifn? config-data-fn)] :post [(satisfies? app/TrapperkeeperApp %)]} (config/initialize-logging! (config-data-fn)) (internal/boot-services* services config-data-fn)) (defn boot-services-with-config "Given a list of ServiceDefinitions and a map containing parsed cli data, create and boot a trapperkeeper app. This function can be used if you prefer to do your own CLI parsing and loading ServiceDefinitions; it circumvents the normal trapperkeeper `bootstrap.cfg` boot process, but still allows trapperkeeper to handle the parsing of your service configuration data. Returns a TrapperkeeperApp instance. Call `run-app` on it if you'd like to block the main thread to wait for a shutdown event." [services config-data] {:pre [(sequential? services) (every? #(satisfies? services/ServiceDefinition %) services) (map? config-data)] :post [(satisfies? app/TrapperkeeperApp %)]} (boot-services-with-config-fn services (constantly config-data))) (schema/defn boot-with-cli-data :- (schema/protocol app/TrapperkeeperApp) "Create and boot a trapperkeeper application. This is accomplished by reading a bootstrap configuration file containing a list of (namespace-qualified) service functions. These functions will be called to generate a service graph for the application; dependency resolution between the services will be handled automatically to ensure that they are started in the correct order. Functions that a service expresses dependencies on will be injected prior to instantiation of a service. The bootstrap config file will be searched for in this order: * At a path specified by the optional command-line argument `--bootstrap-config` * In the current working directory, in a file named `bootstrap.cfg` * On the classpath, in a file named `bootstrap.cfg`. `cli-data` is a map of the command-line arguments and their values. `puppetlabs.kitchensink/cli!` can handle the parsing for you. Their must be a `:config` key in this map which defines the .ini file (or directory of files) used by the configuration service. Returns a TrapperkeeperApp instance. Call `run-app` on it if you'd like to block the main thread to wait for a shutdown event." [cli-data :- common/CLIData] ;; There is a strict order of operations that need to happen here: ;; 1. parse config files ;; 2. initialize logging ;; 3. initialize plugin system ;; 4. bootstrap rest of framework (let [config-data-fn #(config/parse-config-data cli-data)] (config/initialize-logging! (config-data-fn)) (plugins/add-plugin-jars-to-classpath! (cli-data :plugins)) (-> cli-data (bootstrap/find-bootstrap-configs) (bootstrap/parse-bootstrap-configs!) (internal/boot-services* config-data-fn)))) (defn run-app "Given a bootstrapped TrapperKeeper app, let the application run until shut down, which may be triggered by one of several different ways. In all cases, services will be shut down and any exceptions they might throw will be caught and logged." [app] {:pre [(satisfies? app/TrapperkeeperApp app)]} (let [shutdown-reason (internal/wait-for-app-shutdown app)] (when (internal/initiated-internally? shutdown-reason) (internal/call-error-handler! shutdown-reason) (internal/shutdown! (app/app-context app)) (when-let [error (:error shutdown-reason)] (throw error)) (when (= :requested (:cause shutdown-reason)) (select-keys shutdown-reason [::exit]))))) (schema/defn run "Bootstraps a trapperkeeper application and runs it. Blocks the calling thread until trapperkeeper is shut down. `cli-data` is expected to be a map constructed by parsing the CLI args. (see `parse-cli-args`)" [cli-data :- common/CLIData] (let [app (boot-with-cli-data cli-data)] ;; This adds the TrapperkeeperApp instance to the tk-apps list, so that ;; it can be referenced in a remote nREPL session, etc. (swap! internal/tk-apps conj app) (internal/register-sighup-handler) (let [{:keys [::exit] :as result} (run-app app)] ;; Q: If it's appropriate (or even just acceptable) for this ;; removal to be handled via catch/finally, then it'd be a bit ;; simpler to just throw the exit from run-app. (swap! internal/tk-apps (partial remove #{app})) (when exit (throw (ex-info (i18n/trs "Process exit requested") (assoc (::exit result) :kind ::exit))))))) (defn- parse-args "Returns valid CLIData or throws an ::exit." [args] (letfn [(quit [status msg stream ex-msg] (throw (ex-info ex-msg ;; :status and :messages match exit-request-schema {:kind ::exit :status status :messages [[(str msg "\n") stream]]})))] (try (internal/parse-cli-args! (or args ())) (catch ExceptionInfo ex (let [{:keys [kind msg]} (ex-data ex)] (case (some-> kind without-ns) :cli-error (quit 1 msg *err* (i18n/trs "Invalid program arguments")) :cli-help (quit 0 msg *out* (i18n/trs "Command line --help requested")) (throw ex))))))) (defn- handle-exit "Prints any messages provided to the indicated streams, and returns the appropriate process exit status." [{:keys [status messages]}] ;; Check values carefully here since throwing ::exit is a public interface (letfn [(show [stream msg] (binding [*out* stream] (print msg) (flush)))] (doseq [[string stream :as message] messages :when (try (schema/validate [(schema/one schema/Str "message") (schema/one java.io.Writer "stream")] message) (catch Exception ex (show *err* (i18n/trs "Malformed exit message: {0}\n" ex)) false))] (show stream string)) (if (integer? status) status (do (show *err* (i18n/trs "Invalid exit status requested, exiting with 2")) 2)))) (defn main "Launches the trapperkeeper framework. This function blocks until trapperkeeper is shut down. This may be called directly, but is also called by `puppetlabs.trapperkeeper.main/-main` if you use `puppetlabs.trapperkeeper.core` as the `:main` namespace in your leinengen project. Never returns (calls System/exit) after argument processing errors, `--help` requests, or calls to `request-shutdown` that specify a specific process exit status." [& args] {:pre [((some-fn sequential? nil?) args) (every? string? args)]} (try (let [cli (parse-args args)] (try (run cli) (finally (log/debug (i18n/trs "Finished TK main lifecycle, shutting down Clojure agent threads.")) (shutdown-agents)))) (catch ExceptionInfo ex (let [{:keys [kind] :as data} (ex-data ex)] (when-not (= ::exit kind) (throw ex)) (System/exit (handle-exit data)))))) puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/internal.clj000066400000000000000000001016141463756611100272300ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.internal (:import (clojure.lang ExceptionInfo IFn IDeref) (java.lang ArithmeticException NumberFormatException)) (:require [clojure.tools.logging :as log] [beckon] [plumbing.graph :as graph] [slingshot.slingshot :refer [throw+]] [puppetlabs.trapperkeeper.config :refer [config-service get-in-config]] [puppetlabs.trapperkeeper.app :as a] [puppetlabs.trapperkeeper.common :as common] [puppetlabs.trapperkeeper.services :as s] [puppetlabs.kitchensink.core :as ks] [puppetlabs.i18n.core :as i18n] [schema.core :as schema] [clojure.core.async :as async] [clojure.core.async.impl.protocols :as async-prot] [me.raynes.fs :as fs])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Schemas ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def LifeCycleTask {:type (schema/enum :restart :shutdown :boot) :task-function IFn}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Private ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; This is (eww) a global variable that holds a reference to all of the running ;; Trapperkeeper apps in the process. It can be used when connecting via nrepl ;; to allow you to do useful things, and also may be used for other things ;; (such as signal handling). (def tk-apps (atom [])) (def max-pending-lifecycle-events 5) (defn service-graph? "Predicate that tests whether or not the argument is a valid trapperkeeper service graph." [service-graph] (and (map? service-graph) (every? keyword? (keys service-graph)) (every? (some-fn ifn? service-graph?) (vals service-graph)))) (defn inc-restart-counter! "Increments the counter in a restart-file for purposes of supporting HUP behavior" [app] (when-let [restart-file-path (get-in-config (a/get-service app :ConfigService) [:global :restart-file])] (try (if (fs/exists? restart-file-path) (if (and (fs/writeable? restart-file-path) (fs/readable? restart-file-path)) (spit restart-file-path (inc (Long/parseLong (slurp restart-file-path)))) (throw (IllegalStateException. (i18n/trs "Restart file {0} is not readable and/or writeable" restart-file-path)))) (let [dir-path (fs/parent restart-file-path)] (fs/mkdirs dir-path) (spit restart-file-path "1"))) (catch ArithmeticException _e (spit restart-file-path "1") (log/debug (i18n/trs "Number of restarts has exceeded Long/MAX_VALUE, resetting file to 1"))) (catch NumberFormatException _e (spit restart-file-path "1") (log/error (i18n/trs "Restart file is unparseable, resetting file to 1")))))) (defn validate-service-graph! "Validates that a ServiceDefinition contains a valid trapperkeeper service graph. Returns the service definition on success; throws an ::invalid-service-graph if the graph is invalid." [service-def] {:post [(satisfies? s/ServiceDefinition %)]} (when-not (satisfies? s/ServiceDefinition service-def) (throw+ {:type ::invalid-service-graph :message (i18n/trs "Invalid service definition; expected a service definition (created via `service` or `defservice`); found: {0}" (pr-str service-def))})) (if (service-graph? (s/service-map service-def)) service-def (throw+ {:type ::invalid-service-graph :message (i18n/trs "Invalid service graph; service graphs must be nested maps of keywords to functions. Found: {0}" (s/service-map service-def))}))) (defn parse-missing-required-key "Prismatic's graph compilation code throws `ExceptionInfo` objects if required keys are missing somewhere in the graph structure. This includes a map with information about what keys were missing. This function is responsible for interpreting one of those error maps to determine whether it implies that the trapperkeeper service definition was missing a service function. Returns a map containing the name of the service and the name of the missing function, or nil if the error map looks like it represents some other kind of error." [m] {:pre [(map? m)] :post [(or (nil? %) (and (map? %) (contains? % :service-name) (contains? % :service-fn)))]} (let [service-name (first (keys m)) service-fn-name (first (keys (m service-name))) error (get-in m [service-name service-fn-name])] (when (= error 'missing-required-key) {:service-name (name service-name) :service-fn (name service-fn-name)}))) (defn handle-prismatic-exception! "Takes an ExceptionInfo object that was thrown during a prismatic graph compilation / instantiation, and inspects it to see if the error data map represents a missing trapperkeeper service or function. If so, throws a more meaningful exception. If not, re-throws the original exception." [^ExceptionInfo e] {:pre [(instance? ExceptionInfo e)]} (let [data (ex-data e)] (condp = (:error data) :missing-key (throw (RuntimeException. (i18n/trs "Service ''{0}'' not found" (:key data)))) :does-not-satisfy-schema (if-let [error-info (parse-missing-required-key (:failures data))] (throw (RuntimeException. (i18n/trs "Service function ''{0}'' not found in service ''{1}''" (:service-fn error-info) (:service-name error-info)))) (throw e)) (if (sequential? (:error data)) (let [missing-services (keys (ks/filter-map (fn [_ v] (= v 'missing-required-key)) (.error (first (:error data)))))] (if (= 1 (count missing-services)) (throw (RuntimeException. (i18n/trs "Service ''{0}'' not found" (first missing-services)))) (throw (RuntimeException. (i18n/trs "Services ''{0}'' not found" missing-services))))) (throw e))))) (defn compile-graph "Given the merged map of services, compile it into a function suitable for instantiation. Throws an exception if there is a dependency on a service that is not found in the map." [graph-map] {:pre [(service-graph? graph-map)] :post [(ifn? %)]} (try (graph/eager-compile graph-map) (catch ExceptionInfo e (handle-prismatic-exception! e)))) (defn instantiate "Given the compiled graph function, instantiate the application. Throws an exception if there is a dependency on a service function that is not found in the graph." [graph-fn data] {:pre [(ifn? graph-fn) (map? data)] :post [(service-graph? %)]} (try (graph-fn data) (catch ExceptionInfo e (handle-prismatic-exception! e)))) (schema/defn parse-cli-args! :- common/CLIData "Parses the command-line arguments using `puppetlabs.kitchensink.core/cli!`. Hard-codes the command-line arguments expected by trapperkeeper to be: --debug --bootstrap-config --config <.ini file or directory> --plugins --restart-file " [cli-args :- [(schema/maybe schema/Str)]] (let [specs [["-d" "--debug" (i18n/trs "Turns on debug mode")] ["-b" "--bootstrap-config BOOTSTRAP-CONFIG-FILE" (i18n/trs "Path to bootstrap config file")] ["-c" "--config CONFIG-PATH" (i18n/trs "Path to a configuration file or directory of configuration files, or a comma-separated list of such paths. See the documentation for a list of supported file types.")] ["-p" "--plugins PLUGINS-DIRECTORY" (i18n/trs "Path to directory plugin .jars")] ["-r" "--restart-file RESTART-FILE" (i18n/trs "Path to a file whose contents are incremented each time all of the configured services have been started.")]] required []] (first (ks/cli! cli-args specs required)))) (schema/defn ^:always-validate run-lifecycle-fn! "Run a lifecycle function for a service. Required arguments: * app-context: the app context atom; can be updated by the lifecycle fn * lifecycle-fn: a fn from the Lifecycle protocol * lifecycle-fn-name: a string containing the name of the lifecycle fn that is being run. This is only used to produce a readable error message if an error occurs. * service-id: the id of the service that the lifecycle fn is being run on * s: the service that the lifecycle fn is being run on" [app-context :- (schema/atom a/TrapperkeeperAppContext) lifecycle-fn :- IFn lifecycle-fn-name :- schema/Str service-id :- schema/Keyword s :- (schema/protocol s/Service)] (let [;; call the lifecycle function on the service, and keep a reference ;; to the updated context map that it returns updated-ctxt (lifecycle-fn s (get-in @app-context [:service-contexts service-id] {}))] (when-not (map? updated-ctxt) (throw (IllegalStateException. (i18n/trs "Lifecycle function ''{0}'' for service ''{1}'' must return a context map (got: {2})" lifecycle-fn-name (or (s/service-symbol s) service-id) (pr-str updated-ctxt))))) ;; store the updated service context map in the application context atom (swap! app-context assoc-in [:service-contexts service-id] updated-ctxt))) (schema/defn run-lifecycle-fns "Run a lifecycle function for all services. Required arguments: * app-context: the app context atom; can be updated by the lifecycle fn * lifecycle-fn: a fn from the Lifecycle protocol * lifecycle-fn-name: a string containing the name of the lifecycle fn that is being run. This is only used to produce a readable error message if an error occurs. * ordered-services: a list of the services in an order that conforms to their dependency specifications. This ensures that we call the lifecycle functions in the correct order (i.e. a service can be assured that any services it depends on will have their corresponding lifecycle fn called first.)" [app-context :- (schema/atom a/TrapperkeeperAppContext) lifecycle-fn :- IFn lifecycle-fn-name :- schema/Str ordered-services :- a/TrapperkeeperAppOrderedServices] (try (doseq [[service-id s] ordered-services] (log/debug (i18n/trs "Running lifecycle function ''{0}'' for service ''{1}''" lifecycle-fn-name service-id)) (run-lifecycle-fn! app-context lifecycle-fn lifecycle-fn-name service-id s) (log/debug (i18n/trs "Finished running lifecycle function ''{0}'' for service ''{1}''" lifecycle-fn-name service-id))) (catch Throwable t (log/error t (i18n/trs "Error during service {0}!!!" lifecycle-fn-name)) (throw t)))) (schema/defn ^:always-validate initialize-lifecycle-worker :- (schema/protocol async-prot/Channel) "Initializes a 'worker' which will listen for lifecycle-related tasks and perform them on a background thread, to ensure that we aren't executing multiple lifecycle tasks simultaneously." [lifecycle-channel :- (schema/protocol async-prot/Channel) shutdown-channel :- (schema/protocol async-prot/Channel) shutdown-reason-promise :- IDeref] (log/debug (i18n/trs "Initializing lifecycle worker loop.")) (async/go-loop [] (let [[task _chan] (async/alts! [shutdown-channel lifecycle-channel] :priority true)] (schema/validate LifeCycleTask task) (let [{:keys [type task-function]} task] (condp #(contains? %1 %2) type #{:shutdown} (do (try (log/debug (i18n/trs "Received shutdown command, shutting down services")) (async/close! shutdown-channel) (async/close! lifecycle-channel) (log/debug (i18n/trs "Clearing lifecycle worker channels for shutdown.")) ;; drain the channels to ensure that there are ;; no blocking puts, e.g. if a second shutdown request ;; was queued simultaneously (ks/while-let [msg (async/poll! shutdown-channel)] (log/debug (i18n/trs "Shutdown in progress, ignoring message on shutdown channel: {0}" (:type msg)))) (ks/while-let [msg (async/poll! lifecycle-channel)] (log/debug (i18n/trs "Shutdown in progress, ignoring message on main lifecycle channel: {0}" (:type msg)))) (task-function) (log/debug (i18n/trs "Service shutdown complete, exiting lifecycle worker loop")) (catch Exception e (log/debug e (i18n/trs "Exception caught during shutdown!")))) :done) #{:boot :restart} (do (try (log/debug (i18n/trs "Lifecycle worker executing {0} lifecycle task." type)) (task-function) (log/debug (i18n/trs "Lifecycle worker completed {0} lifecycle task; awaiting next task." type)) (catch Exception e (log/debug e (i18n/trs "Exception caught in lifecycle worker loop")) (deliver shutdown-reason-promise {:cause :service-error :error e}))) (recur)) (do (deliver shutdown-reason-promise {:cause :service-error :error (IllegalStateException. (i18n/trs "Unrecognized lifecycle task: %s" task))}) (recur))))))) (defn restart-tk-apps "Call restart on all tk apps." [apps] (log/info (i18n/trs "SIGHUP handler restarting TK apps.")) (doseq [app apps] (let [{:keys [lifecycle-channel]} @(a/app-context app) restart-fn #(a/restart app)] (when-not (async/offer! lifecycle-channel {:type :restart :task-function restart-fn}) (log/warn (i18n/trs "Ignoring new SIGHUP restart requests; too many requests queued ({0})" max-pending-lifecycle-events)))))) (defn register-sighup-handler "Register a handler for SIGHUP that restarts all trapperkeeper apps. The default handler terminates the process, so we always overwrite that. This function is idempotent." ([] (register-sighup-handler @tk-apps)) ([apps] (log/debug (i18n/trs "Registering SIGHUP handler for restarting TK apps")) (reset! (beckon/signal-atom "HUP") #{(partial restart-tk-apps apps)}))) ;;;; Application Shutdown Support ;;;; ;;;; The next section of this namespace ;;;; provides top-level functions to be called by TrapperKeeper internally ;;;; and a shutdown service for application services to utilize. ;;;; ;;;; Performing the actual shutdown is relatively easy, but dealing with ;;;; exceptions during shutdown and providing various ways for services ;;;; to trigger shutdown make up most of the code in this namespace. ;;;; ;;;; During development of this namespace we wanted to ensure that the ;;;; shutdown sequence occurred on the main thread and not on some ;;;; service's worker thread. Because of this, a Clojure `promise` is ;;;; used to pass contextual shutdown information back to the main thread, ;;;; at which point it is responsible for calling back into this namespace ;;;; to perform the appropriate shutdown steps. ;;;; ;;;; As a consequence of the implementation, the main blocking behavior of ;;;; TrapperKeeper is defined here by `wait-for-app-shutdown`. This function ;;;; simply attempts to dereference the above mentioned promise, which ;;;; until it is realized (e.g. by a call to `deliver`) will block, and the ;;;; delivered value returned. This value will contain contextual information ;;;; regarding the cause of the shutdown, and is intended to be passed back ;;;; in to the top-level functions that perform various shutdown steps. (def exit-request-schema "A process exit request like {:status 7 :messages [[\"something for stderr\n\" *err*]] [\"something for stdout\n\" *out*]] [\"something else for stderr\n\" *err*]]" {:status schema/Int :messages [[(schema/one schema/Str "message") (schema/one java.io.Writer "stream")]]}) (def ^{:private true :doc "The possible causes for shutdown to be initiated."} shutdown-causes #{:requested :service-error :jvm-shutdown-hook}) (schema/defn request-shutdown* "Initiate the normal shutdown of TrapperKeeper. This is asynchronous. It is assumed that `wait-for-app-shutdown` has been called and is blocking. Intended to be used by application services (likely their worker threads) to programatically trigger application shutdown. Note that this is exposed via the `shutdown-service`. See the protocol documentation for addiitonal information." ([shutdown-reason-promise] (request-shutdown* shutdown-reason-promise {})) ([shutdown-reason-promise opts :- {(schema/optional-key :puppetlabs.trapperkeeper.core/exit) exit-request-schema}] (deliver shutdown-reason-promise (merge {:cause :requested} (select-keys opts [:puppetlabs.trapperkeeper.core/exit]))))) (defn shutdown-on-error* "A higher-order function that is intended to be used as a wrapper around some logic `f` in your services. It will wrap your application logic in a `try/catch` block that will cause TrapperKeeper to initiate an error shutdown if an exception occurs in your block. This is generally intended to be used on worker threads that your service may launch. If an optional `on-error-fn` is provided, it will be executed if `f` throws an exception, but before the primary shutdown sequence begins." ([shutdown-reason-promise app-context svc-id f] (shutdown-on-error* shutdown-reason-promise app-context svc-id f nil)) ([shutdown-reason-promise app-context svc-id f on-error-fn] (try ; This schema check would normally be handled via schematizing the fn itself; ; however, this function needs to never throw an exception - since it is often ; called as a wrapper around everything inside a `future`, it is important ; that this function ; never throw anything (like a schema validation error), ; since it is likely to just get lost in a `future`. Instead, ; invalid arguments will simply cause the shutdown promise to be delivered. (schema/validate a/TrapperkeeperAppContext @app-context) (schema/validate schema/Keyword svc-id) (assert (contains? (:service-contexts @app-context) svc-id)) (schema/validate IFn f) (schema/validate (schema/maybe IFn) on-error-fn) (f) (catch Throwable t (log/error t (i18n/trs "shutdown-on-error triggered because of exception!")) (deliver shutdown-reason-promise {:cause :service-error :error t :on-error-fn (when on-error-fn (partial on-error-fn (get @app-context svc-id)))}))))) (defprotocol ShutdownService (get-shutdown-reason [this]) (wait-for-shutdown [this]) (request-shutdown [this] [this opts] "Asynchronously triggers normal shutdown. A specific exit status and final messages to display can be requested by associating an exit-request-schema compatible value with the :puppetlabs.trapperkeeper.core/exit key in the opts map.") (shutdown-on-error [this svc-id f] [this svc-id f on-error] "Higher-order function to execute application logic and trigger shutdown in the event of an exception")) (schema/defn shutdown-service "Provides various functions for triggering application shutdown programatically. Primarily intended to serve application services, though TrapperKeeper also uses this service internally and therefore is always available in the application graph. Provides: * :get-shutdown-reason - Get a map containing the shutdown reason delivered to the shutdown service. If no shutdown reason has been delivered, this function would return nil. * :wait-for-shutdown - Block the calling thread until a shutdown reason has been delivered to the shutdown service * :request-shutdown - Asynchronously trigger normal shutdown * :shutdown-on-error - Higher-order function to execute application logic and trigger shutdown in the event of an exception For more information, see `request-shutdown` and `shutdown-on-error`." [shutdown-reason-promise :- IDeref app-context :- (schema/atom a/TrapperkeeperAppContext)] (s/service ShutdownService [] (get-shutdown-reason [this] (when (realized? shutdown-reason-promise) @shutdown-reason-promise)) (wait-for-shutdown [this] (deref shutdown-reason-promise)) (request-shutdown [this] (request-shutdown* shutdown-reason-promise)) (request-shutdown [this opts] (request-shutdown* shutdown-reason-promise opts)) (shutdown-on-error [this svc-id f] (shutdown-on-error* shutdown-reason-promise app-context svc-id f)) (shutdown-on-error [this svc-id f on-error] (shutdown-on-error* shutdown-reason-promise app-context svc-id f on-error)))) (schema/defn ^:always-validate shutdown! :- [Throwable] "Perform shutdown calling the `stop` lifecycle function on each service, in reverse order (to account for dependency relationships). Returns collection of exceptions thrown during shutdown sequence execution." [app-context :- (schema/atom a/TrapperkeeperAppContext)] (log/info (i18n/trs "Beginning shutdown sequence")) (let [{:keys [ordered-services shutdown-channel lifecycle-worker]} @app-context errors-chan (async/promise-chan) shutdown-fn (fn [] (let [results (doall (keep (fn [[service-id s]] (try (run-lifecycle-fn! app-context s/stop "stop" service-id s) nil (catch Exception e (log/error e (i18n/trs "Encountered error during shutdown sequence")) e))) (reverse ordered-services)))] (async/put! errors-chan results)))] (log/trace (i18n/trs "Putting shutdown message on shutdown channel.")) (async/>!! shutdown-channel {:type :shutdown :task-function shutdown-fn}) ;; wait for the channel to send us the return value so we know it's done (log/trace (i18n/trs "Waiting for response to shutdown message from lifecycle worker.")) (if (not (nil? (async/graph service-map) ;; when we instantiate the graph, we pass in the context atom. graph-instance (instantiate compiled-graph {:tk-app-context app-context :tk-service-refs service-refs}) ;; dereference the atom of service references, since we don't need to update it ;; any further services-by-id @service-refs ordered-services (map (fn [[service-id _]] [service-id (services-by-id service-id)]) graph)] (swap! app-context assoc :services-by-id services-by-id :ordered-services ordered-services) (doseq [svc-id (keys services-by-id)] (swap! app-context assoc-in [:service-contexts svc-id] {})) ;; finally, create the app instance (reify a/TrapperkeeperApp (a/get-service [_this protocol] (services-by-id (keyword protocol))) (a/service-graph [_this] graph-instance) (a/app-context [_this] app-context) (a/check-for-errors! [this] (throw-app-error-if-exists! this)) (a/init [this] (run-lifecycle-fns app-context s/init "init" ordered-services) this) (a/start [this] (run-lifecycle-fns app-context s/start "start" ordered-services) (inc-restart-counter! this) this) (a/stop [this] (a/stop this false)) (a/stop [this throw?] (let [errors (shutdown! app-context)] (if (and throw? (seq errors)) (let [msg (i18n/trs "Error during app shutdown!")] (log/error msg) (throw (ex-info msg {:errors errors}))) this))) (a/restart [this] (try (run-lifecycle-fns app-context s/stop "stop" (reverse ordered-services)) (doseq [svc-id (keys services-by-id)] (swap! app-context assoc-in [:service-contexts svc-id] {})) (run-lifecycle-fns app-context s/init "init" ordered-services) (run-lifecycle-fns app-context s/start "start" ordered-services) (inc-restart-counter! this) this (catch Throwable t (deliver shutdown-reason-promise {:cause :service-error :error t}))))))) (schema/defn ^:always-validate boot-services-for-app** "Boots services for a TK app. WARNING: This should only ever be called on the lifecycle-worker, presumably via `boot-services-for-app*`" [result-promise :- IDeref app :- (schema/protocol a/TrapperkeeperApp)] (let [{:keys [shutdown-reason-promise]} @(a/app-context app)] (try (a/init app) (a/start app) (catch Throwable t (deliver shutdown-reason-promise {:cause :service-error :error t}))) (deliver result-promise app))) (schema/defn ^:always-validate boot-services-for-app* :- (schema/protocol a/TrapperkeeperApp) [app :- (schema/protocol a/TrapperkeeperApp)] (let [lifecycle-channel (:lifecycle-channel @(a/app-context app)) lifecycle-promise (promise) boot-fn (partial boot-services-for-app** lifecycle-promise app)] (async/>!! lifecycle-channel {:type :boot :task-function boot-fn}) @lifecycle-promise app)) (schema/defn ^:always-validate boot-services* :- (schema/protocol a/TrapperkeeperApp) "Given the services to run and the map of configuration data, create the TrapperkeeperApp and boot the services. Returns the TrapperkeeperApp." [services :- [(schema/protocol s/ServiceDefinition)] config-data-fn :- IFn] (let [app (try (build-app* services config-data-fn) (catch Throwable t (log/error t (i18n/trs "Error during app buildup!")) (throw t)))] (boot-services-for-app* app) app)) puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/logging.clj000066400000000000000000000073551463756611100270510ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.logging (:import [ch.qos.logback.classic Level LoggerContext PatternLayout] (ch.qos.logback.core ConsoleAppender) (org.slf4j Logger LoggerFactory) (ch.qos.logback.classic.joran JoranConfigurator)) (:require [clojure.stacktrace :refer [print-cause-trace]] [clojure.tools.logging :as log] [puppetlabs.i18n.core :as i18n])) (defn logging-context ^LoggerContext [] ;; in practice, this returns ch.qos.logback.classic.LoggerContext ;; which the other functions below assume (LoggerFactory/getILoggerFactory)) (defn reset-logging [] (.reset (logging-context))) (def root-logger-name Logger/ROOT_LOGGER_NAME) (defn root-logger ^ch.qos.logback.classic.Logger [] (LoggerFactory/getLogger ^String root-logger-name)) (defn catch-all-logger "A logging function useful for catch-all purposes, that is, to ensure that a log message gets in front of a user the best we can even if that means duplicated output. This is really only suitable for _last-ditch_ exception handling, where we want to make sure an exception is logged (because nobody higher up in the stack will log it for us)." ([exception] (catch-all-logger exception (i18n/trs "Uncaught exception"))) ([exception message] (print-cause-trace exception) (flush) (log/error exception message))) (defn create-console-appender "Instantiates and returns a logging appender configured to write to the console, using the standard logging configuration. `level` is an optional argument (of type `org.apache.log4j.Level`) indicating the logging threshold for the new appender. Defaults to `DEBUG`." ([] (create-console-appender Level/DEBUG)) ([level] {:pre [(instance? Level level)]} (let [layout (PatternLayout.)] (doto layout (.setContext (logging-context)) (.setPattern "%d %-5p [%t] [%c{2}] %m%n") (.start)) (doto (ConsoleAppender.) (.setContext (logging-context)) (.setLayout layout) (.start))))) (defn add-console-logger! "Adds a console logger to the current logging configuration, and ensures that the root logger is set to log at the logging level of the new logger or finer. `level` is an optional argument (of type `org.apache.log4j.Level`) indicating the logging threshold for the new logger. Defaults to `DEBUG`." ([] (add-console-logger! Level/DEBUG)) ([level] {:pre [(instance? Level level)]} (let [root (root-logger)] (.addAppender root (create-console-appender level)) (when (> (.toInt (.getLevel root)) (.toInt ^Level level)) (.setLevel root level))))) (defn configure-logger! "Reconfigures the current logger based on the supplied configuration. Supplied configuration can be a file path, url, file, InputStream, or InputSource. It is passed along unchanged to `doConfigure` for JoranConfigurator. For more information, see the documentation for ch.qos.logback.core.classic.joran.JoranConfigurator." [logging-conf] (let [configurator (JoranConfigurator.) context (logging-context)] (.setContext configurator context) (.reset context) (.doConfigure configurator logging-conf))) (defn configure-logging! "Takes a file path, url, file, InputStream, or InputSource which can define how to configure the logging system. This is passed unchanged to the `doConfigure` method for the underlying JoranConfigurator class. Also takes an optional `debug` flag which turns on debug logging." ([logging-conf] (configure-logging! logging-conf false)) ([logging-conf debug] (when logging-conf (configure-logger! logging-conf)) (when debug (add-console-logger! Level/DEBUG) (log/debug (i18n/trs "Debug logging enabled"))))) puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/main.clj000066400000000000000000000002641463756611100263370ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.main (:gen-class)) (defn -main [& args] (require 'puppetlabs.trapperkeeper.core) (apply (resolve 'puppetlabs.trapperkeeper.core/main) args)) puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/plugins.clj000066400000000000000000000110241463756611100270700ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.plugins (:import (java.util.jar JarEntry JarFile) (java.io File)) (:require [clojure.java.io :refer [file]] [clojure.tools.logging :as log] [puppetlabs.kitchensink.classpath :as kitchensink] [puppetlabs.i18n.core :as i18n])) (defn- should-process? "Helper for `process-file`. Answers whether or not the duplicate detection code should process a file with the given name." [^String name] (and ;; ignore directories (not (or (.isDirectory (file name)) (.endsWith name "/"))) ; necessary for directories in .jars ;; don't care about anything in META-INF (not (.startsWith name "META-INF")) ;; lein includes project.clj ... no thank you (not (= name "project.clj")))) (defn- handle-duplicate! "Helper for `process-file`; handles a found duplicate. Throws an exception if the duplicate is a .class or .clj file. Otherwise, logs a warning and returns the accumulator." [container-filename acc ^String filename] (let [error-msg (i18n/trs "Class or namespace {0} found in both {1} and {2}" filename container-filename (acc filename))] (if (or (.endsWith filename ".class") (.endsWith filename ".clj")) (throw (IllegalArgumentException. ^String error-msg)) ;; It is common to have other conflicts (besides classes and clojure ;; namespaces), especially during development (for example, ;; jetty-servlet and jetty-http both contain an `about.html` - ;; these conflicts don't exist in the uberjar anyway, ;; and likely aren't important. (log/warn error-msg))) acc) (defn- process-file "Helper for `process-container`. Processes a file and adds it to the accumulator if it is a .class or .clj file we care about." [container-filename acc filename] (if (should-process? filename) (if (contains? acc filename) (handle-duplicate! container-filename acc filename) (assoc acc filename container-filename)) acc)) (defn- process-container "Helper for `verify-no-duplicate-resources`. Processes a .jar file or directory that contains classes and/or .clj sources and builds up map of .class/.clj filenames -> container names." [acc container-filename] (let [file (file container-filename)] (if (.exists file) (let [filenames (if (.isDirectory file) (map #(.getPath ^File %) (file-seq file)) (map #(.getName ^JarEntry %) (enumeration-seq (.entries (JarFile. file)))))] (reduce (partial process-file container-filename) acc filenames)) acc))) ; There may be directories on the classpath that do not exist. (defn jars-in-dir "Given a path to a directory on disk, returns a collection of all of the .jar files contained in that directory (not recursive)." [^File dir] {:pre [(instance? File dir)] :post [(coll? %) (every? (partial instance? File) %)]} (filter #(.endsWith (.getAbsolutePath ^File %) ".jar") (.listFiles dir))) (defn verify-no-duplicate-resources "Examines all resources on the classpath and contained in the given directory and checks for duplicates. A resource in this context is defined as a .class or .clj file. Throws an Exception if any duplicates are found." [dir] {:pre [(instance? File dir)]} (let [plugin-jars (jars-in-dir dir) classpath (System/getProperty "java.class.path") ;; When running as an uberjar, this system property contains only ;; the path to the uberjar (-classpath is ignored). classpath-containers (if (.contains classpath ":") (.split classpath ":") [classpath]) all-containers (concat plugin-jars classpath-containers)] (reduce process-container {} all-containers))) (defn add-plugin-jars-to-classpath! "Add all of .jar files contained in the plugins directory (specified by the '--plugins' CLI argument) to the classpath." [plugins-path] (when plugins-path (let [plugins (file plugins-path)] (if (.exists plugins) (do (verify-no-duplicate-resources plugins) (doseq [^File jar (jars-in-dir plugins)] (log/info (i18n/trs "Adding plugin {0} to classpath." (.getAbsolutePath jar))) (kitchensink/add-classpath jar) (kitchensink/add-classpath jar (clojure.lang.RT/baseLoader)))) (let [^String msg (i18n/trs "Plugins directory {0} does not exist" plugins-path)] (throw (IllegalArgumentException. msg))))))) puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/services.clj000066400000000000000000000152021463756611100272340ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.services (:require [plumbing.core :refer [fnk]] [puppetlabs.trapperkeeper.services-internal :as si] [schema.core :as schema] [puppetlabs.i18n.core :as i18n])) (defprotocol Lifecycle "Lifecycle functions for a service. All services satisfy this protocol, and the lifecycle functions for each service will be called at the appropriate phase during the application lifecycle." (init [this context] "Initialize the service, given a context map. Must return the (possibly modified) context map.") (start [this context] "Start the service, given a context map. Must return the (possibly modified) context map.") (stop [this context] "Stop the service, given a context map. Must return the (possibly modified) context map.")) (defprotocol Service "Common functions available to all services" (service-id [this] "An identifier for the service") (service-context [this] "Returns the context map for this service") (get-service [this service-id] "Returns the service with the given service id. Throws if service not present") (maybe-get-service [this service-id] "Returns the service with the given service id. Returns nil if service not present") (get-services [this] "Returns a sequence containing all of the services in the app") (service-included? [this service-id] "Returns true or false whether service is included") (service-symbol [this] "The namespaced symbol of the service definition, or `nil` if no service symbol was provided.")) (defprotocol ServiceDefinition "A service definition. This protocol is for internal use only. The service is not usable until it is instantiated (via `boot!`)." (service-def-id [this] "An identifier for the service") (service-map [this] "The map of service functions for the graph")) (def lifecycle-fn-names (map :name (vals (:sigs Lifecycle)))) (defn name-with-attributes "This is a plate of warm and nutritious copypasta of clojure.tools.macro/name-with-attributes. Without this modified version, name-with-attributes consumes a dependency map when a protocol is not present in a defservice invocation. This version of the function double checks a map that might be metadata and ignores it if it conforms to the DependencyMap schema. Forgive me." [name macro-args] (let [[docstring macro-args] (if (string? (first macro-args)) [(first macro-args) (next macro-args)] [nil macro-args]) [attr macro-args] (if (and (map? (first macro-args)) (schema/check si/DependencyMap (first macro-args))) [(first macro-args) (next macro-args)] [{} macro-args]) attr (if docstring (assoc attr :doc docstring) attr) attr (if (meta name) (conj (meta name) attr) attr)] [(with-meta name attr) macro-args])) (defmacro service "Create a Trapperkeeper ServiceDefinition. First argument (optional) is a protocol indicating the list of functions that this service exposes for use by other Trapperkeeper services. Second argument is the dependency list; this should be a vector of vectors. Each inner vector should begin with a keyword representation of the name of the service protocol that the service depends upon. All remaining items in the inner vectors should be symbols representing functions that should be imported from the service. The remaining arguments should be function definitions for this service, specified in the format that is used by a normal clojure `reify`. The legal list of functions that may be specified includes whatever functions are defined by this service's protocol (if it has one), plus the list of functions in the `Lifecycle` protocol." [& forms] (let [{:keys [service-sym service-protocol-sym service-id service-fn-map dependencies fns-map]} (si/parse-service-forms! lifecycle-fn-names forms) output-schema (si/build-output-schema (keys service-fn-map))] `(reify ServiceDefinition (service-def-id [this] ~service-id) ;; service map for prismatic graph (service-map [this] {~service-id ;; the main service fnk for the app graph. we add metadata to the fnk ;; arguments list to specify an explicit output schema for the fnk (fnk service-fnk# :- ~output-schema ~(conj dependencies 'tk-app-context 'tk-service-refs) (let [svc# (reify Service (service-id [this#] ~service-id) (service-context [this#] (get-in ~'@tk-app-context [:service-contexts ~service-id] {})) (get-service [this# service-id#] (or (get-in ~'@tk-app-context [:services-by-id service-id#]) (throw (IllegalArgumentException. (i18n/trs "Call to ''get-service'' failed; service ''{0}'' does not exist." service-id#))))) (maybe-get-service [this# service-id#] (get-in ~'@tk-app-context [:services-by-id service-id#] nil)) (get-services [this#] (-> ~'@tk-app-context :services-by-id (dissoc :ConfigService :ShutdownService) vals)) (service-symbol [this#] '~service-sym) (service-included? [this# service-id#] (not (nil? (get-in ~'@tk-app-context [:services-by-id service-id#] nil)))) Lifecycle ~@(si/fn-defs fns-map lifecycle-fn-names) ~@(when service-protocol-sym `(~service-protocol-sym ~@(si/fn-defs fns-map (vals service-fn-map)))))] (swap! ~'tk-service-refs assoc ~service-id svc#) (si/build-service-map ~service-fn-map svc#)))})))) (defmacro defservice [svc-name & forms] (let [service-sym (symbol (name (ns-name *ns*)) (name svc-name)) [svc-name forms] (name-with-attributes svc-name forms)] `(def ~svc-name (service {:service-symbol ~service-sym} ~@forms)))) puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/services/000077500000000000000000000000001463756611100265425ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/services/nrepl/000077500000000000000000000000001463756611100276625ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/services/nrepl/nrepl_service.clj000066400000000000000000000046331463756611100332220ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.services.nrepl.nrepl-service (:require [clojure.tools.logging :as log] [nrepl.server :as nrepl] [puppetlabs.kitchensink.core :refer [to-bool]] [puppetlabs.trapperkeeper.core :refer [defservice]] [puppetlabs.i18n.core :as i18n])) ;; If no port is specified in the config then 7888 is used (def ^{:private true} default-nrepl-port 7888) (def ^{:private true} default-bind-addr "0.0.0.0") (def ^{:private true} default-middlewares []) (defn- parse-middlewares-if-necessary [middlewares] (if (string? middlewares) (read-string middlewares) (map symbol middlewares))) (defn- process-middlewares [middlewares] (let [middlewares (parse-middlewares-if-necessary middlewares)] (doseq [middleware (map #(symbol (namespace %)) middlewares)] (require middleware)) (let [resolved (map #(resolve %) middlewares)] (apply nrepl/default-handler resolved)))) (defn process-config [get-in-config] {:enabled? (to-bool (get-in-config [:nrepl :enabled])) :port (get-in-config [:nrepl :port] default-nrepl-port) :bind (get-in-config [:nrepl :host] default-bind-addr) :handler (process-middlewares (get-in-config [:nrepl :middlewares] default-middlewares))}) (defn- startup-nrepl [get-in-config] (let [{:keys [enabled? port bind handler]} (process-config get-in-config)] (if enabled? (do (log/info (i18n/trs "Starting nREPL service on {0} port {1}" bind port)) (nrepl/start-server :port port :bind bind :handler handler)) (log/info (i18n/trs "nREPL service disabled, not starting"))))) (defn- shutdown-nrepl [nrepl-server] (when nrepl-server (log/info (i18n/trs "Shutting down nREPL service")) (nrepl/stop-server nrepl-server))) (defservice nrepl-service "The nREPL trapperkeeper service starts up a Clojure network REPL (nREPL) server attached to the running trapperkeeper process. It is configured in the following manner: [nrepl] enabled=true port=7888 host=0.0.0.0 The nrepl service will only start if enabled is set to true, and the port specified which port nREPL should bind to. If no port is specified then the default port of 7888 is used." [[:ConfigService get-in-config]] (init [this context] (let [nrepl-server (startup-nrepl get-in-config)] (assoc context :nrepl-server nrepl-server))) (stop [this context] (shutdown-nrepl (context :nrepl-server)) context)) puppetlabs-trapperkeeper-d1f1135/src/puppetlabs/trapperkeeper/services_internal.clj000066400000000000000000000361211463756611100311330ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.services-internal (:import (clojure.lang IFn Namespace)) (:require [clojure.set :refer [difference union intersection]] [schema.core :as schema] [puppetlabs.kitchensink.core :as ks] [puppetlabs.i18n.core :as i18n])) (def optional-dep-default "This value is used in place of an optional service when the service is not included. In the future, it may be a more interesting or useful dummy service." nil) (defn protocol? "A predicate to determine whether or not an object is a protocol definition" [p] ;; there might be a better way to do this, but it seems to work (and (map? p) (contains? p :on) (instance? Class (resolve (:on p))))) (defn fn-sig? "A predicate to determine whether or not a form represents a valid function signature in the context of a service definition." [sig] (and (seq? sig) (> (count sig) 1) (symbol? (first sig)) (vector? (second sig)))) (def Protocol "A schema to determine whether or not an object is a protocol definition." (schema/pred protocol?)) (def Symbol "Interal schema type for validating symbol values." (schema/pred symbol?)) (def Var "Interal schema type for validating var values." (schema/pred var?)) (def ServiceFnMap "Internal schema for a service's map of function references." {schema/Keyword IFn}) (def FnsMap "Internal schema for a map of a service's function body forms." {schema/Keyword [(schema/pred fn-sig?)]}) (def InternalServiceMap "Schema defining TK's internal representation of a service. This is used while processing the service macro." {:service-sym (schema/maybe Symbol) :service-protocol-sym (schema/maybe Symbol) :service-id schema/Keyword :service-fn-map ServiceFnMap :dependencies (schema/pred sequential?) :fns-map FnsMap}) (def ProtocolandDependenciesMap "Schema defining the map used to represent the protocol and dependencies of a service." {:fns [(schema/pred seq?)] :dependencies (schema/pred vector?) :service-protocol-sym (schema/maybe Symbol)}) (def ServiceMap "Schema defining the map used to represent a schema in the output service map." {schema/Keyword IFn}) (def DependencyMap "Schema for a map detailing optional vs. required dependencies for a service." {:optional (schema/pred vector?) :required (schema/pred vector?)}) (schema/defn ^:always-validate var->symbol :- Symbol "Returns a symbol for the var, including its namespace" [fn-var :- Var] (let [m (meta fn-var)] (symbol (str (.name ^Namespace (:ns m))) (str (:name m))))) (schema/defn ^:always-validate transform-deps-map :- (schema/pred vector?) "Given a map of required and optional dependencies, return a vector that adheres to Prismatic Graph's binding syntax. Specifically, optional dependencies are transformed into {ServiceName nil}." [deps :- DependencyMap] (loop [optional (:optional deps) output (:required deps)] (if (empty? optional) output (let [dep (first optional) optional-form (array-map dep optional-dep-default)] (recur (rest optional) (conj output optional-form)))))) (schema/defn ^:always-validate find-prot-and-deps-forms! :- ProtocolandDependenciesMap "Given the forms passed to the service macro, find the service protocol (if one is provided), the dependency list, and the function definitions. Throws `IllegalArgumentException` if the forms do not represent a valid service. Returns a map containing the protocol, dependency list, and fn forms." [forms :- (schema/pred seq?)] (let [f (if ((some-fn symbol? map? vector?) (first forms)) (first forms) (throw (IllegalArgumentException. (i18n/trs "Invalid service definition; first form must be protocol or dependency list; found ''{0}''" (pr-str (first forms)))))) service-protocol-sym (if (symbol? f) f nil) forms (if (nil? service-protocol-sym) forms (rest forms)) ff (first forms) deps (cond (vector? ff) ff (map? ff) (transform-deps-map ff) :else (throw (IllegalArgumentException. (i18n/trs "Invalid service definition; expected dependency list following protocol, found: ''{0}''" (pr-str ff)))))] (if (every? seq? (rest forms)) {:service-protocol-sym service-protocol-sym :dependencies deps :fns (rest forms)} (throw (IllegalArgumentException. (i18n/trs "Invalid service definition; expected function definitions following dependency list, invalid value: ''{0}''" (pr-str (first (filter #(not (seq? %)) (rest forms)))))))))) (schema/defn ^:always-validate validate-protocol-sym! :- Protocol "Given a var, validate that the var exists and that its value is a protocol. Throws `IllegalArgumentException` if the var does not exist or if its value is something other than a protocol. Returns the protocol." [sym :- Symbol var :- (schema/maybe Var)] (when-not var (throw (IllegalArgumentException. (i18n/trs "Unrecognized service protocol ''{0}''" sym)))) (let [protocol (var-get var)] (when-not (protocol? protocol) (throw (IllegalArgumentException. (i18n/trs "Specified service protocol ''{0}'' does not appear to be a protocol!" sym)))) protocol)) (schema/defn ^:always-validate validate-protocol-fn-names! :- nil "Validate that the service protocol does not define any functions that have the same name as a lifecycle function. Throws `IllegalArgumentException` if it does." [service-protocol-sym :- Symbol service-fn-names :- [Symbol] lifecycle-fn-names :- [Symbol]] (let [collisions (intersection (set (map name service-fn-names)) (set (map name lifecycle-fn-names)))] (when-not (empty? collisions) (throw (IllegalArgumentException. (i18n/trs "Service protocol ''{0}'' includes function named ''{1}'', which conflicts with lifecycle function by same name" (name service-protocol-sym) (first collisions))))))) (schema/defn ^:always-validate validate-provided-fns! :- nil "Validate that the seq of fns specified in a service body does not include any functions that are not part of the service protocol. Throws `IllegalArgumentException` otherwise." [service-protocol-sym :- (schema/maybe Symbol) service-fns :- #{schema/Keyword} provided-fns :- #{schema/Keyword}] (when (and (nil? service-protocol-sym) (> (count provided-fns) 0)) (throw (IllegalArgumentException. (i18n/trs "Service attempts to define function ''{0}'', but does not provide protocol" (name (first provided-fns)))))) (let [extras (difference provided-fns service-fns)] (when-not (empty? extras) (throw (IllegalArgumentException. (i18n/trs "Service attempts to define function ''{0}'', which does not exist in protocol ''{1}''" (name (first extras)) (name service-protocol-sym))))))) (schema/defn ^:always-validate validate-required-fns! :- nil "Given a map of fn forms and a list of required function names, validate that all of the required functions are defined. Throws `IllegalArgumentException` otherwise." [protocol-sym :- Symbol required-fn-names :- (schema/maybe [Symbol]) fns-map :- FnsMap] (doseq [fn-name required-fn-names] (let [fn-name (ks/without-ns (keyword fn-name))] (when-not (contains? fns-map fn-name) (throw (IllegalArgumentException. (i18n/trs "Service does not define function ''{0}'', which is required by protocol ''{1}''" (name fn-name) (name protocol-sym)))))))) (schema/defn ^:always-validate add-default-lifecycle-fn :- FnsMap "Given a map of fns defined by a service, and the name of a lifecycle function, check to see if the fns map includes an implementation of the lifecycle function. If not, add a default implementation." [fns-map :- FnsMap fn-name :- Symbol] {:post [(= (ks/keyset %) (conj (ks/keyset fns-map) (keyword fn-name)))]} (if (contains? fns-map (keyword fn-name)) fns-map (assoc fns-map (keyword fn-name) (list (cons fn-name '([this context] context)))))) (schema/defn ^:always-validate add-default-lifecycle-fns :- FnsMap "Given a map of fns comprising a service body, add in a default implementation for any lifecycle functions that are not overridden." [lifecycle-fn-names :- [Symbol] fns-map :- FnsMap] {:post [(= (ks/keyset %) (union (ks/keyset fns-map) (set (map keyword lifecycle-fn-names))))]} (reduce add-default-lifecycle-fn fns-map lifecycle-fn-names)) (schema/defn ^:always-validate fn-defs :- [(schema/pred seq?)] "Given a map of all of the function forms from a service definition, and a list of function names, return a sequence of all of the forms (including multi-arity forms) for the given function names." [fns-map :- FnsMap fn-names :- [Symbol]] (reduce (fn [acc fn-name] (let [sigs (fns-map (ks/without-ns (keyword fn-name)))] (concat acc sigs))) '() fn-names)) (schema/defn ^:always-validate build-service-map :- ServiceMap "Given a map from service protocol function names (keywords) to service protocol functions, and a service instance, build up a map of partial functions closing over the service instance" [service-fn-map :- ServiceFnMap svc] ;; TODO this would be checked against Service, but it lives in services (into {} (map (fn [[fn-name fn-sym]] [fn-name (partial fn-sym svc)]) service-fn-map))) (schema/defn ^:always-validate build-output-schema :- {schema/Keyword (schema/pred (partial = IFn))} "Given a list of service protocol function names (keywords), build up the prismatic output schema for the service (a map from keywords to `IFn`)." [service-fn-names :- (schema/maybe [schema/Keyword])] (reduce (fn [acc fn-name] (assoc acc fn-name IFn)) {} service-fn-names)) (schema/defn ^:always-validate build-fns-map! :- FnsMap "Given the list of fn forms from the service body, build up a map of service fn forms. The keys of the map will be keyword representations of the function names, and the values will be the fn forms. The final map will include default implementations of any lifecycle functions that aren't overridden in the service body. Throws `IllegalArgumentException` if the fn forms do not match the protocol." [service-protocol-sym :- (schema/maybe Symbol) service-fn-names :- (schema/maybe (schema/pred coll?)) lifecycle-fn-names :- [Symbol] fns :- [(schema/pred seq?)]] {:post [(= (ks/keyset %) (union (set (map (comp ks/without-ns keyword) service-fn-names)) (set (map keyword lifecycle-fn-names))))]} (when service-protocol-sym (validate-protocol-fn-names! service-protocol-sym service-fn-names lifecycle-fn-names)) (let [fns-map (->> (reduce (fn [acc f] ; second element should be a vector - params to the fn (when-not (vector? (second f)) ; macro was used incorrectly - perhaps the user ; mistakenly tried to insert a docstring, like: ; `(service-fn "docs about service-fn..." [this] ... )` (throw (Exception. (i18n/trs "Incorrect macro usage: service functions must be defined the same as a call to `reify`, eg: `(my-service-fn [this other-args] ...)`")))) (let [k (keyword (first f)) cur (acc k)] (if cur (assoc acc k (cons f cur)) (assoc acc k (list f))))) {} fns) (add-default-lifecycle-fns lifecycle-fn-names))] (validate-provided-fns! service-protocol-sym (set (map (comp ks/without-ns keyword) service-fn-names)) (difference (ks/keyset fns-map) (set (map (comp ks/without-ns keyword) lifecycle-fn-names)))) (when service-protocol-sym (validate-required-fns! service-protocol-sym service-fn-names fns-map)) fns-map)) (schema/defn ^:always-validate get-service-id :- schema/Keyword "Generate service id based on service protocol symbol. Returns the keyword of the symbol if the symbol is not nil; returns a keyword for a generated symbol otherwise." [service-protocol-sym :- (schema/maybe Symbol)] (ks/without-ns (if service-protocol-sym (keyword service-protocol-sym) (keyword (gensym "tk-service"))))) (schema/defn ^:always-validate get-service-fn-map :- ServiceFnMap "Get a map of service fns based on a protocol. Keys will be keywords of the function names, values will be the protocol functions. Returns an empty map if the protocol symbol is nil." [service-protocol-sym :- (schema/maybe Symbol)] (if service-protocol-sym (let [service-protocol-var (resolve service-protocol-sym) service-protocol (validate-protocol-sym! service-protocol-sym service-protocol-var)] (reduce (fn [acc fn-name] (assoc acc (keyword (name fn-name)) fn-name)) {} (mapv var->symbol (keys (:method-builders service-protocol))))) {})) (schema/defn ^:always-validate parse-service-forms! :- InternalServiceMap "Parse the forms provided to the `service` macro. Return a map containing all of the data necessary to implement the macro: :service-protocol-sym - the service protocol symbol (or nil if there is no protocol) :service-id - a unique identifier (keyword) for the service :service-fn-map - a map of symbols for the names of the functions provided by the protocol :dependencies - a vector (using fnk binding syntax) of deps or a map of :required and :optional deps. :fns-map - a map of all of the fn definition forms in the service" [lifecycle-fn-names :- [(schema/pred symbol?)] forms :- (schema/pred seq?)] (let [service-sym (:service-symbol (first forms)) service-sym (if (symbol? service-sym) service-sym nil) forms (if (nil? service-sym) forms (rest forms)) {:keys [service-protocol-sym dependencies fns]} (find-prot-and-deps-forms! forms) service-id (get-service-id service-protocol-sym) service-fn-map (get-service-fn-map service-protocol-sym) fns-map (build-fns-map! service-protocol-sym (vals service-fn-map) lifecycle-fn-names fns)] {:service-sym service-sym :service-protocol-sym service-protocol-sym :service-id service-id :service-fn-map service-fn-map :dependencies dependencies :fns-map fns-map})) puppetlabs-trapperkeeper-d1f1135/test/000077500000000000000000000000001463756611100200575ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/000077500000000000000000000000001463756611100222365ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/000077500000000000000000000000001463756611100251075ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/bootstrap_test.clj000066400000000000000000000620411463756611100306600ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.bootstrap-test (:require [clojure.java.io :as io] [clojure.string :as string] [clojure.test :refer :all] [me.raynes.fs :as fs] [puppetlabs.kitchensink.classpath :refer [with-additional-classpath-entries]] [puppetlabs.kitchensink.core :refer [without-ns]] [puppetlabs.trapperkeeper.app :refer [get-service]] [puppetlabs.trapperkeeper.bootstrap :as bootstrap] [puppetlabs.trapperkeeper.examples.bootstrapping.test-services :refer [hello-world test-fn test-fn-three test-fn-two]] [puppetlabs.trapperkeeper.logging :refer [reset-logging]] [puppetlabs.trapperkeeper.services :refer [service-map]] [puppetlabs.trapperkeeper.testutils.bootstrap :refer [bootstrap-with-empty-config parse-and-bootstrap]] [puppetlabs.trapperkeeper.testutils.logging :refer [with-test-logging]] [schema.test :as schema-test] [slingshot.slingshot :refer [try+]])) (use-fixtures :once schema-test/validate-schemas ;; Without this, "lein test NAMESPACE" and :only invocations may fail. (fn [f] (reset-logging) (f))) (deftest bootstrapping (testing "Valid bootstrap configurations" (let [bootstrap-config "./dev-resources/bootstrapping/cli/bootstrap.cfg" app (parse-and-bootstrap bootstrap-config)] (testing "Can load a service based on a valid bootstrap config string" (let [test-svc (get-service app :TestService) hello-world-svc (get-service app :HelloWorldService)] (is (= (test-fn test-svc) :cli)) (is (= (hello-world hello-world-svc) "hello world")))) (with-additional-classpath-entries ["./dev-resources/bootstrapping/classpath"] (testing "Looks for bootstrap config on classpath (dev-resources)" (with-test-logging (let [app (bootstrap-with-empty-config) test-svc (get-service app :TestService) hello-world-svc (get-service app :HelloWorldService)] (is (logged? #"Loading bootstrap config from classpath: 'file:/.*dev-resources/bootstrapping/classpath/bootstrap.cfg'" :debug)) (is (= (test-fn test-svc) :classpath)) (is (= (hello-world hello-world-svc) "hello world"))))) (testing "Gives precedence to bootstrap config in cwd" (let [cwd-config (io/file (System/getProperty "user.dir") "bootstrap.cfg") test-config (io/file "./dev-resources/bootstrapping/cwd/bootstrap.cfg")] ;; This test used to set the user.dir property to the dev-resources dir above, ;; however in Java 11 it is illegal to set user.dir at runtime. (is (not (.exists cwd-config)) "A bootstrap config file exists in the cwd, cannot reliably test cwd bootstrap loading!") (try (io/copy test-config cwd-config) (with-test-logging (let [app (bootstrap-with-empty-config) test-svc (get-service app :TestService) hello-world-svc (get-service app :HelloWorldService)] (is (logged? #"Loading bootstrap config from current working directory: '.*/bootstrap.cfg'" :debug)) (is (= (test-fn test-svc) :cwd)) (is (= (hello-world hello-world-svc) "hello world")))) (finally (io/delete-file cwd-config))))) (testing "Gives precedence to bootstrap config specified as CLI arg" (with-test-logging (let [bootstrap-path "./dev-resources/bootstrapping/cli/bootstrap.cfg" app (bootstrap-with-empty-config ["--bootstrap-config" bootstrap-path]) test-svc (get-service app :TestService) hello-world-svc (get-service app :HelloWorldService)] (is (logged? (format "Loading bootstrap configs:\n%s" bootstrap-path) :debug)) (is (= (test-fn test-svc) :cli)) (is (= (hello-world hello-world-svc) "hello world"))))))) (testing "Invalid bootstrap configurations" (testing "Bootstrap config path specified on CLI does not exist" (let [cfg-path "./dev-resources/bootstrapping/cli/non-existent-bootstrap.cfg"] (is (thrown-with-msg? IllegalArgumentException #"Specified bootstrap config file does not exist: '.*non-existent-bootstrap.cfg'" (bootstrap-with-empty-config ["--bootstrap-config" cfg-path]))))) (testing "No bootstrap config found" (is (thrown-with-msg? IllegalStateException #"Unable to find bootstrap.cfg file via --bootstrap-config command line argument, current working directory, or on classpath" (bootstrap-with-empty-config))) (let [got-expected-exception (atom false)] (try+ (bootstrap-with-empty-config ["--bootstrap-config" nil]) (catch map? m (is (contains? m :kind)) (is (= :cli-error (without-ns (:kind m)))) (is (= :puppetlabs.kitchensink.core/cli-error (:kind m))) (is (contains? m :msg)) (is (re-find #"Missing required argument for.*--bootstrap-config" (m :msg))) (reset! got-expected-exception true))) (is (true? @got-expected-exception)))) (testing "Bad line in bootstrap config file" (let [bootstrap-config "./dev-resources/bootstrapping/cli/invalid_entry_bootstrap.cfg"] (is (thrown-with-msg? IllegalArgumentException #"(?is)Invalid line in bootstrap.*This is not a legit line" (parse-and-bootstrap bootstrap-config))))) (testing "Invalid service graph" (let [bootstrap-config "./dev-resources/bootstrapping/cli/invalid_service_graph_bootstrap.cfg"] (is (thrown-with-msg? IllegalArgumentException #"Invalid service definition;" (parse-and-bootstrap bootstrap-config))))))) (testing "comments allowed in bootstrap config file" (let [bootstrap-config "./dev-resources/bootstrapping/cli/bootstrap_with_comments.cfg" service-maps (->> bootstrap-config bootstrap/parse-bootstrap-config! (map service-map))] (is (= (count service-maps) 2)) (is (contains? (first service-maps) :HelloWorldService)) (is (contains? (second service-maps) :TestService))))) (deftest empty-bootstrap (testing "Empty bootstrap causes error" (testing "single bootstrap file" (let [bootstrap-config "./dev-resources/bootstrapping/cli/empty_bootstrap.cfg"] (is (thrown-with-msg? Exception (re-pattern (str "No entries found in any supplied bootstrap file\\(s\\):\n" "./dev-resources/bootstrapping/cli/empty_bootstrap.cfg")) (bootstrap/parse-bootstrap-config! bootstrap-config))))) (testing "multiple bootstrap files" (let [bootstraps ["./dev-resources/bootstrapping/cli/split_bootstraps/empty/empty1.cfg" "./dev-resources/bootstrapping/cli/split_bootstraps/empty/empty2.cfg"]] (is (thrown-with-msg? Exception (re-pattern (str "No entries found in any supplied bootstrap file\\(s\\):\n" (string/join "\n" bootstraps))) (bootstrap/parse-bootstrap-configs! bootstraps))))))) (deftest multiple-bootstrap-files (testing "Multiple bootstrap files can be specified directly on the command line" (with-test-logging (let [bootstrap-one "./dev-resources/bootstrapping/cli/split_bootstraps/one/bootstrap_one.cfg" bootstrap-two "./dev-resources/bootstrapping/cli/split_bootstraps/two/bootstrap_two.cfg" bootstrap-path (format "%s,%s" bootstrap-one bootstrap-two) app (bootstrap-with-empty-config ["--bootstrap-config" bootstrap-path]) test-svc (get-service app :TestService) test-svc-two (get-service app :TestServiceTwo) test-svc-three (get-service app :TestServiceThree) hello-world-svc (get-service app :HelloWorldService)] (is (logged? (format "Loading bootstrap configs:\n%s\n%s" bootstrap-one bootstrap-two) :debug)) (is (= (test-fn test-svc) :cli)) (is (= (test-fn-two test-svc-two) :two)) (is (= (test-fn-three test-svc-three) :three)) (is (= (hello-world hello-world-svc) "hello world"))))) (testing "A path containing multiple .cfg files can be specified on the command line" (with-test-logging (let [bootstrap-path "./dev-resources/bootstrapping/cli/split_bootstraps/both/" app (bootstrap-with-empty-config ["--bootstrap-config" bootstrap-path]) test-svc (get-service app :TestService) test-svc-two (get-service app :TestServiceTwo) test-svc-three (get-service app :TestServiceThree) hello-world-svc (get-service app :HelloWorldService)] (is (logged? ; We can't know what order it will find the files on disk, so just ; look for a partial match with the path we gave TK. (re-pattern (format "Loading bootstrap configs:\n%s" (fs/absolute bootstrap-path))) :debug)) (is (= (test-fn test-svc) :cli)) (is (= (test-fn-two test-svc-two) :two)) (is (= (test-fn-three test-svc-three) :three)) (is (= (hello-world hello-world-svc) "hello world"))))) (testing "A path containing both a file and a folder can be specified on the command line" (with-test-logging (let [bootstrap-one-dir "./dev-resources/bootstrapping/cli/split_bootstraps/one/" bootstrap-one "./dev-resources/bootstrapping/cli/split_bootstraps/one/bootstrap_one.cfg" bootstrap-two "./dev-resources/bootstrapping/cli/split_bootstraps/two/bootstrap_two.cfg" bootstrap-path (format "%s,%s" bootstrap-one-dir bootstrap-two) app (bootstrap-with-empty-config ["--bootstrap-config" bootstrap-path]) test-svc (get-service app :TestService) test-svc-two (get-service app :TestServiceTwo) test-svc-three (get-service app :TestServiceThree) hello-world-svc (get-service app :HelloWorldService)] (is (logged? (format "Loading bootstrap configs:\n%s\n%s" (fs/absolute bootstrap-one) bootstrap-two) :debug)) (is (= (test-fn test-svc) :cli)) (is (= (test-fn-two test-svc-two) :two)) (is (= (test-fn-three test-svc-three) :three)) (is (= (hello-world hello-world-svc) "hello world")))))) (deftest bootstrap-path-with-spaces (testing "Ensure that a bootstrap config can be loaded with a path that contains spaces" (with-test-logging (let [bootstrap-path "./dev-resources/bootstrapping/cli/path with spaces/bootstrap.cfg" app (bootstrap-with-empty-config ["--bootstrap-config" bootstrap-path]) test-svc (get-service app :TestService) hello-world-svc (get-service app :HelloWorldService)] (is (logged? (format "Loading bootstrap configs:\n%s" bootstrap-path) :debug)) (is (= (test-fn test-svc) :cli)) (is (= (hello-world hello-world-svc) "hello world"))))) (testing "Multiple bootstrap files can be specified with spaces in the names" (with-test-logging (let [bootstrap-one "./dev-resources/bootstrapping/cli/split_bootstraps/spaces/bootstrap with spaces one.cfg" bootstrap-two "./dev-resources/bootstrapping/cli/split_bootstraps/spaces/bootstrap with spaces two.cfg" bootstrap-path (format "%s,%s" bootstrap-one bootstrap-two) app (bootstrap-with-empty-config ["--bootstrap-config" bootstrap-path]) test-svc (get-service app :TestService) test-svc-two (get-service app :TestServiceTwo) test-svc-three (get-service app :TestServiceThree) hello-world-svc (get-service app :HelloWorldService)] (is (logged? (format "Loading bootstrap configs:\n%s\n%s" bootstrap-one bootstrap-two) :debug)) (is (= (test-fn test-svc) :cli)) (is (= (test-fn-two test-svc-two) :two)) (is (= (test-fn-three test-svc-three) :three)) (is (= (hello-world hello-world-svc) "hello world")))))) (deftest duplicate-service-entries (testing "duplicate bootstrap entries are allowed" (let [bootstrap-path "./dev-resources/bootstrapping/cli/duplicate_entries.cfg" app (bootstrap-with-empty-config ["--bootstrap-config" bootstrap-path]) hello-world-svc (get-service app :HelloWorldService)] (is (= (hello-world hello-world-svc) "hello world"))))) (deftest duplicate-service-definitions (testing "Duplicate service definitions causes error with filename and line numbers" (let [bootstraps ["./dev-resources/bootstrapping/cli/duplicate_services/duplicates.cfg"]] (is (thrown-with-msg? IllegalArgumentException (re-pattern (str "Duplicate implementations found for service protocol ':TestService':\n" ".*/duplicates.cfg:2\n" "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service\n" ".*/duplicates.cfg:3\n" "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service\n" "Duplicate implementations.*\n" ".*/duplicates.cfg:5\n" ".*test-service-two\n" ".*/duplicates.cfg:6\n" ".*test-service-two-duplicate")) (bootstrap/parse-bootstrap-configs! bootstraps)))) (testing "Duplicate service definitions between two files throws error" (let [bootstraps ["./dev-resources/bootstrapping/cli/duplicate_services/split_one.cfg" "./dev-resources/bootstrapping/cli/duplicate_services/split_two.cfg"]] (is (thrown-with-msg? IllegalArgumentException (re-pattern (str "Duplicate implementations found for service protocol ':TestService':\n" ".*/split_one.cfg:2\n" "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service\n" ".*/split_two.cfg:2\n" "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service\n" "Duplicate implementations.*\n" ".*/split_one.cfg:4\n" ".*test-service-two-duplicate\n" ".*/split_two.cfg:4\n" ".*test-service-two")) (bootstrap/parse-bootstrap-configs! bootstraps))))))) (deftest config-file-in-jar (testing "Bootstrapping via a config file contained in a .jar as command line option" (let [jar (io/file "./dev-resources/bootstrapping/jar/this-jar-contains-a-bootstrap-config-file.jar") bootstrap-url (str "jar:file:///" (.getAbsolutePath jar) "!/bootstrap.cfg")] ;; just test that this bootstrap config file can be read successfully ;; (ie, this does not throw an exception) (bootstrap-with-empty-config ["--bootstrap-config" bootstrap-url])))) (deftest config-from-classpath-test (testing "can locate bootstrap file on the classpath" (let [bootstrap-file "./dev-resources/bootstrapping/classpath/bootstrap.cfg" bootstrap-uri (str "file:" (.getCanonicalPath (io/file bootstrap-file)))] (with-additional-classpath-entries ["./dev-resources/bootstrapping/classpath/"] (let [found-bootstraps (bootstrap/config-from-classpath)] (is (= 1 (count found-bootstraps))) (is (= bootstrap-uri (first found-bootstraps))))))) (testing "can locate bootstrap file contained in a .jar on the classpath" (let [jar "./dev-resources/bootstrapping/jar/this-jar-contains-a-bootstrap-config-file.jar" jar-uri (str "file:" (.getAbsolutePath (io/file jar))) expected-resource-uri (format "jar:%s!/bootstrap.cfg" jar-uri)] (with-additional-classpath-entries ["./dev-resources/bootstrapping/jar/this-jar-contains-a-bootstrap-config-file.jar"] (let [found-bootstraps (bootstrap/config-from-classpath)] (is (= 1 (count found-bootstraps))) (is (= expected-resource-uri (first found-bootstraps)))))))) (deftest parse-bootstrap-config-test (testing "Missing service namespace logs warning" (with-test-logging (let [bootstrap-config "./dev-resources/bootstrapping/cli/fake_namespace_bootstrap.cfg"] (bootstrap/parse-bootstrap-config! bootstrap-config) (is (logged? (str "Unable to load service 'non-existent-service/test-service' from " "./dev-resources/bootstrapping/cli/fake_namespace_bootstrap.cfg:3") :warn))))) (testing "Missing service definition logs warning" (with-test-logging (let [bootstrap-config "./dev-resources/bootstrapping/cli/missing_definition_bootstrap.cfg"] (bootstrap/parse-bootstrap-config! bootstrap-config) (is (logged? (str "Unable to load service " "'puppetlabs.trapperkeeper.examples.bootstrapping.test-services/non-existent-service' " "from ./dev-resources/bootstrapping/cli/missing_definition_bootstrap.cfg:3") :warn))))) (testing "errors are thrown with line number and file" ; Load a bootstrap with a bad service graph to generate an error (let [bootstrap "./dev-resources/bootstrapping/cli/invalid_service_graph_bootstrap.cfg"] (is (thrown-with-msg? IllegalArgumentException (re-pattern (str "Problem loading service " "'puppetlabs.trapperkeeper.examples.bootstrapping.test-services/invalid-service-graph-service' " "from ./dev-resources/bootstrapping/cli/invalid_service_graph_bootstrap.cfg:1:\n" "Invalid service definition")) (bootstrap/parse-bootstrap-config! bootstrap)))))) (deftest get-annotated-bootstrap-entries-test (testing "file with comments" (let [bootstraps ["./dev-resources/bootstrapping/cli/bootstrap_with_comments.cfg"] entries (bootstrap/get-annotated-bootstrap-entries bootstraps)] (is (= [{:bootstrap-file "./dev-resources/bootstrapping/cli/bootstrap_with_comments.cfg" :entry "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service" :line-number 2} {:bootstrap-file "./dev-resources/bootstrapping/cli/bootstrap_with_comments.cfg" :entry "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/foo-test-service" :line-number 5}] entries)))) (testing "multiple bootstrap files" (let [bootstraps ["./dev-resources/bootstrapping/cli/split_bootstraps/both/bootstrap_one.cfg" "./dev-resources/bootstrapping/cli/split_bootstraps/both/bootstrap_two.cfg"] entries (bootstrap/get-annotated-bootstrap-entries bootstraps)] (is (= [{:bootstrap-file "./dev-resources/bootstrapping/cli/split_bootstraps/both/bootstrap_one.cfg" :entry "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service" :line-number 1} {:bootstrap-file "./dev-resources/bootstrapping/cli/split_bootstraps/both/bootstrap_one.cfg" :entry "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service" :line-number 2} {:bootstrap-file "./dev-resources/bootstrapping/cli/split_bootstraps/both/bootstrap_two.cfg" :entry "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-two" :line-number 1} {:bootstrap-file "./dev-resources/bootstrapping/cli/split_bootstraps/both/bootstrap_two.cfg" :entry "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/test-service-three" :line-number 2}] entries))))) (deftest find-duplicates-test (testing "correct duplicates found" (let [items [{:important-key :one :other-key 2} {:important-key :one :other-key 3} {:important-key :two :other-key 4} {:important-key :three :other-key 5}]] ; List of key value pairs (is (= {:one [{:important-key :one :other-key 2} {:important-key :one :other-key 3}]} (bootstrap/find-duplicates items :important-key)))))) (deftest check-duplicate-service-implementations!-test (testing "no duplicate service implementations does not throw error" (let [configs ["./dev-resources/bootstrapping/cli/bootstrap.cfg"] bootstrap-entries (bootstrap/get-annotated-bootstrap-entries configs) resolved-services (bootstrap/resolve-services! bootstrap-entries)] (bootstrap/check-duplicate-service-implementations! resolved-services bootstrap-entries))) (testing "duplicate service implementations throws error" (let [configs ["./dev-resources/bootstrapping/cli/duplicate_services/duplicates.cfg"] bootstrap-entries (bootstrap/get-annotated-bootstrap-entries configs) resolved-services (bootstrap/resolve-services! bootstrap-entries)] (is (thrown-with-msg? IllegalArgumentException #"Duplicate implementations found for service protocol ':TestService'" (bootstrap/check-duplicate-service-implementations! resolved-services bootstrap-entries)))))) (deftest remove-duplicate-entries-test (testing "single bootstrap with all duplicates" (testing "only the first duplicate found is kept" (let [configs ["./dev-resources/bootstrapping/cli/duplicate_entries.cfg"] bootstrap-entries (bootstrap/get-annotated-bootstrap-entries configs)] (is (= [{:bootstrap-file "./dev-resources/bootstrapping/cli/duplicate_entries.cfg" :entry "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service" :line-number 1}] (bootstrap/remove-duplicate-entries bootstrap-entries)))))) (testing "two copies of the same set of services" (let [configs ["./dev-resources/bootstrapping/cli/bootstrap.cfg" "./dev-resources/bootstrapping/cli/bootstrap.cfg"] bootstrap-entries (bootstrap/get-annotated-bootstrap-entries configs)] (is (= [{:bootstrap-file "./dev-resources/bootstrapping/cli/bootstrap.cfg" :entry "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service" :line-number 1} {:bootstrap-file "./dev-resources/bootstrapping/cli/bootstrap.cfg" :entry "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service" :line-number 2}] (bootstrap/remove-duplicate-entries bootstrap-entries)))))) (deftest read-config-test (testing "basic config" (let [config "./dev-resources/bootstrapping/cli/bootstrap.cfg"] (is (= ["puppetlabs.trapperkeeper.examples.bootstrapping.test-services/cli-test-service" "puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service"] (bootstrap/read-config config))))) (testing "jar uri" (let [jar "./dev-resources/bootstrapping/jar/this-jar-contains-a-bootstrap-config-file.jar" config (str "jar:file:///" (.getAbsolutePath (io/file jar)) "!/bootstrap.cfg")] ; The bootstrap in the jar contains an empty line at the end (is (= ["puppetlabs.trapperkeeper.examples.bootstrapping.test-services/hello-world-service" ""] (bootstrap/read-config config))))) (testing "malformed uri is wrapped in our exception" (let [config "\n"] (is (thrown-with-msg? IllegalArgumentException #"Specified bootstrap config file does not exist" (bootstrap/read-config config))))) (testing "Non-absolute uri is wrapped in our exception" ; TODO This path is currently interpreted as a URI because TK checks ; if it's a file, and if not, attemps to load as a URI (let [config "./not-a-file"] (is (thrown-with-msg? IllegalArgumentException #"Specified bootstrap config file does not exist" (println (bootstrap/read-config config)))))) (testing "Non-existent file in URI is wrapped in our exception" (let [config "file:///not-a-file"] (is (thrown-with-msg? IllegalArgumentException #"Specified bootstrap config file does not exist" (bootstrap/read-config config)))))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/config_test.clj000066400000000000000000000147251463756611100301160ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.config-test (:import (java.io FileNotFoundException)) (:require [clojure.test :refer :all] [puppetlabs.trapperkeeper.testutils.bootstrap :refer [bootstrap-services-with-cli-data with-app-with-cli-data]] [puppetlabs.trapperkeeper.app :refer [get-service]] [puppetlabs.trapperkeeper.services :refer [defservice]] [puppetlabs.trapperkeeper.config :refer [load-config]] [schema.test :as schema-test])) (use-fixtures :once schema-test/validate-schemas) (defprotocol ConfigTestService (test-fn [this ks]) (test-fn2 [this]) (get-in-config [this ks] [this ks default])) (defservice test-service ConfigTestService [[:ConfigService get-in-config get-config]] (test-fn [this ks] (get-in-config ks)) (test-fn2 [this] (get-config)) (get-in-config [this ks] (get-in-config ks)) (get-in-config [this ks default] (get-in-config ks default))) (deftest test-config-service (testing "Fails if config path doesn't exist" (is (thrown-with-msg? FileNotFoundException #"Configuration path './foo/bar/baz' must exist and must be readable." (bootstrap-services-with-cli-data [test-service] {:config "./foo/bar/baz"})))) (testing "Can read values from a single .ini file" (with-app-with-cli-data app [test-service] {:config "./dev-resources/config/file/config.ini"} (let [test-svc (get-service app :ConfigTestService)] (is (= (test-fn test-svc [:foo :setting1]) "foo1")) (is (= (test-fn test-svc [:foo :setting2]) "foo2")) (is (= (test-fn test-svc [:bar :setting1]) "bar1")) (testing "`get-config` function" (is (= (test-fn2 test-svc) {:foo {:setting2 "foo2" :setting1 "foo1"} :bar {:setting1 "bar1"} :debug false})))))) (testing "Can read values from a single .edn file" (with-app-with-cli-data app [test-service] {:config "./dev-resources/config/file/config.edn"} (let [test-svc (get-service app :ConfigTestService)] (testing "`get-config` function" (is (= {:debug false :foo {:bar "barbar" :baz "bazbaz" :bam 42 :bap {:boozle "boozleboozle" :bip [1 2 {:hi "there"} 3]}}} (test-fn2 test-svc))))))) (testing "Can parse comma-separated configs" (with-app-with-cli-data app [test-service] {:config (str "./dev-resources/config/mixeddir/baz.ini," "./dev-resources/config/mixeddir/bar.conf")} (let [test-svc (get-service app :ConfigTestService)] (is (= {:debug false, :baz {:setting1 "baz1", :setting2 "baz2"} :bar {:junk "thingz" :nesty {:mappy {:hi "there" :stuff [1 2 {:how "areyou"} 3]}}}} (test-fn2 test-svc)))))) (testing "Conflicting comma-separated configs fail with error" (is (thrown-with-msg? IllegalArgumentException #"Duplicate configuration entry: \[:foo :baz\]" (bootstrap-services-with-cli-data [test-service] {:config (str "./dev-resources/config/conflictdir1/config.ini," "./dev-resources/config/conflictdir1/config.conf")})))) (testing "Error results when second of two comma-separated configs is malformed" (is (thrown-with-msg? FileNotFoundException #"Configuration path 'blob.conf' must exist and must be readable." (bootstrap-services-with-cli-data [test-service] {:config (str "./dev-resources/config/conflictdir1/config.ini," "blob.conf")})))) ;; NOTE: other individual file formats are tested in `typesafe-test` (testing "Can read values from a directory of .ini files" (with-app-with-cli-data app [test-service] {:config "./dev-resources/config/inidir"} (let [test-svc (get-service app :ConfigTestService)] (is (= (test-fn test-svc [:baz :setting1]) "baz1")) (is (= (test-fn test-svc [:baz :setting2]) "baz2")) (is (= (test-fn test-svc [:bam :setting1]) "bam1"))))) (testing "A proper default value is returned if a key can't be found" (with-app-with-cli-data app [test-service] {:config "./dev-resources/config/inidir"} (let [test-svc (get-service app :ConfigTestService)] (is (= (get-in-config test-svc [:doesnt :exist] "foo") "foo"))))) (testing "Can read values from a directory of mixed config files" (with-app-with-cli-data app [test-service] {:config "./dev-resources/config/mixeddir"} (let [test-svc (get-service app :ConfigTestService) cfg (test-fn2 test-svc)] (is (= {:debug false :taco {:burrito [1, 2] :nacho "cheese"} :foo {:bar "barbar" :baz "bazbaz" :meaningoflife 42} :baz {:setting1 "baz1" :setting2 "baz2"} :bar {:nesty {:mappy {:hi "there" :stuff [1 2 {:how "areyou"} 3]}} :junk "thingz"}} cfg))))) (testing "An error is thrown if duplicate settings exist" (doseq [invalid-config-dir ["./dev-resources/config/conflictdir1" "./dev-resources/config/conflictdir2" "./dev-resources/config/conflictdir3"]] (is (thrown-with-msg? IllegalArgumentException #"Duplicate configuration entry: \[:foo :baz\]" (bootstrap-services-with-cli-data [test-service] {:config invalid-config-dir}))))) (testing "Can call load-config directly" (is (= {:taco {:burrito [1, 2] :nacho "cheese"} :foo {:bar "barbar" :baz "bazbaz" :meaningoflife 42} :baz {:setting1 "baz1" :setting2 "baz2"} :bar {:nesty {:mappy {:hi "there" :stuff [1 2 {:how "areyou"} 3]}} :junk "thingz"}} (load-config "./dev-resources/config/mixeddir"))))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/core_test.clj000066400000000000000000000137251463756611100276000ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.core-test (:require [clojure.test :refer :all] [puppetlabs.kitchensink.core :as ks] [puppetlabs.trapperkeeper.app :refer [get-service]] [puppetlabs.trapperkeeper.config :as config] [puppetlabs.trapperkeeper.internal :refer [parse-cli-args!]] [puppetlabs.trapperkeeper.services :refer [service]] [puppetlabs.trapperkeeper.testutils.bootstrap :as testutils] [puppetlabs.trapperkeeper.testutils.logging :as logging] [schema.test :as schema-test] [slingshot.slingshot :refer [try+]])) (use-fixtures :each schema-test/validate-schemas logging/reset-logging-config-after-test) (defprotocol FooService (foo [this])) (deftest dependency-error-handling (testing "missing service dependency throws meaningful message and logs error" (let [broken-service (service [[:MissingService f]] (init [this context] (f) context))] (logging/with-test-logging (is (thrown-with-msg? RuntimeException #"Service ':MissingService' not found" (testutils/bootstrap-services-with-empty-config [broken-service]))) (is (logged? #"Error during app buildup!" :error) "App buildup error message not logged")))) (testing "missing service function throws meaningful message and logs error" (let [test-service (service FooService [] (foo [this] "foo")) broken-service (service [[:FooService bar]] (init [this context] (bar) context))] (logging/with-test-logging (is (thrown-with-msg? RuntimeException #"Service function 'bar' not found in service 'FooService" (testutils/bootstrap-services-with-empty-config [test-service broken-service]))) (is (logged? #"Error during app buildup!" :error) "App buildup error message not logged"))) (try (macroexpand '(puppetlabs.trapperkeeper.services/service puppetlabs.trapperkeeper.core-test/FooService [] (init [this context] context))) (catch RuntimeException e (let [cause (-> e Throwable->map :cause)] (is (re-matches #"Service does not define function 'foo'.*" cause))))))) (deftest test-main (testing "Parsed CLI data" (let [bootstrap-file "/fake/path/bootstrap.cfg" config-dir "/fake/config/dir" restart-file "/fake/restart/file" cli-data (parse-cli-args! ["--debug" "--bootstrap-config" bootstrap-file "--config" config-dir "--restart-file" restart-file])] (is (= bootstrap-file (cli-data :bootstrap-config))) (is (= config-dir (cli-data :config))) (is (= restart-file (cli-data :restart-file))) (is (cli-data :debug)))) (testing "Invalid CLI data" (let [got-expected-exception (atom false)] (try+ (parse-cli-args! ["--invalid-argument"]) (catch map? m (is (contains? m :kind)) (is (= :cli-error (ks/without-ns (:kind m)))) (is (= :puppetlabs.kitchensink.core/cli-error (:kind m))) (is (contains? m :msg)) (is (re-find #"Unknown option.*--invalid-argument" (m :msg))) (reset! got-expected-exception true))) (is (true? @got-expected-exception)))) (testing "TK should allow the user to omit the --config arg" ;; Make sure args will be parsed if no --config arg is provided; will throw an exception if not (parse-cli-args! []) (is (true? true))) (testing "TK should use an empty config if none is specified" ;; Make sure data will be parsed if no path is provided; will throw an exception if not. (config/parse-config-data {}) (is (true? true)))) (deftest test-cli-args (testing "debug mode is off by default" (testutils/with-app-with-empty-config app [] (let [config-service (get-service app :ConfigService)] (is (false? (config/get-in-config config-service [:debug])))))) (testing "--debug puts TK in debug mode" (testutils/with-app-with-cli-args app [] ["--config" testutils/empty-config "--debug"] (let [config-service (get-service app :ConfigService)] (is (true? (config/get-in-config config-service [:debug])))))) (testing "TK should accept --plugins arg" ;; Make sure --plugins is allowed; will throw an exception if not. (parse-cli-args! ["--config" "yo mama" "--plugins" "some/plugin/directory"]))) (deftest restart-file-config (let [tk-config-file-with-restart (ks/temp-file "restart-global" ".conf") tk-restart-file "/my/tk-restart-file" cli-restart-file "/my/cli-restart-file"] (spit tk-config-file-with-restart (format "global: {\nrestart-file: %s\n}" tk-restart-file)) (testing "restart-file setting comes from TK config when CLI arg absent" (let [config (config/parse-config-data {:config (str tk-config-file-with-restart)})] (is (= tk-restart-file (get-in config [:global :restart-file]))))) (testing "restart-file setting comes from CLI arg when no TK config setting" (let [empty-tk-config-file (ks/temp-file "empty" ".conf") config (config/parse-config-data {:config (str empty-tk-config-file) :restart-file cli-restart-file})] (is (= cli-restart-file (get-in config [:global :restart-file]))))) (testing "restart-file setting comes from CLI arg even when set in TK config" (let [config (config/parse-config-data {:config (str tk-config-file-with-restart) :restart-file cli-restart-file})] (is (= cli-restart-file (get-in config [:global :restart-file]))))))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/custom_exit_behavior_test.clj000066400000000000000000000011361463756611100330630ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.custom-exit-behavior-test (:require [puppetlabs.trapperkeeper.core :as core])) (defprotocol CustomExitBehaviorTestService) (core/defservice custom-exit-behavior-test-service CustomExitBehaviorTestService [[:ShutdownService request-shutdown]] (init [this context] context) (start [this context] (request-shutdown {::core/exit {:messages [["Some excitement!\n" *out*] ["More excitement!\n" *err*]] :status 7}}) context) (stop [this context] context)) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/examples/000077500000000000000000000000001463756611100267255ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/examples/bootstrapping/000077500000000000000000000000001463756611100316205ustar00rootroot00000000000000test_services.clj000066400000000000000000000020621463756611100351150ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/examples/bootstrapping(ns puppetlabs.trapperkeeper.examples.bootstrapping.test-services (:require [puppetlabs.trapperkeeper.core :refer [defservice]])) (defn invalid-service-graph-service [] {:test-service "hi"}) (defprotocol HelloWorldService (hello-world [this])) (defprotocol TestService (test-fn [this])) (defprotocol TestServiceTwo (test-fn-two [this])) (defprotocol TestServiceThree (test-fn-three [this])) (defservice hello-world-service HelloWorldService [] (hello-world [this] "hello world")) (defservice foo-test-service TestService [] (test-fn [this] :foo)) (defservice classpath-test-service TestService [] (test-fn [this] :classpath)) (defservice cwd-test-service TestService [] (test-fn [this] :cwd)) (defservice cli-test-service TestService [] (test-fn [this] :cli)) (defservice test-service-two TestServiceTwo [] (test-fn-two [this] :two)) (defservice test-service-two-duplicate TestServiceTwo [] (test-fn-two [this] :two)) (defservice test-service-three TestServiceThree [] (test-fn-three [this] :three)) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/internal_test.clj000066400000000000000000000110511463756611100304520ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.internal-test (:require [clojure.test :refer :all] [puppetlabs.trapperkeeper.core :as tk] [puppetlabs.trapperkeeper.app :as tk-app] [puppetlabs.trapperkeeper.internal :as internal] [puppetlabs.trapperkeeper.testutils.bootstrap :as testutils] [puppetlabs.trapperkeeper.testutils.logging :as logging])) (deftest test-queued-restarts (testing "main lifecycle and calls to `restart-tk-apps` are not executed concurrently" (let [boot-promise (promise) lifecycle-events (atom []) svc (tk/service [] (init [this context] (swap! lifecycle-events conj :init) context) (start [this context] @boot-promise (swap! lifecycle-events conj :start) context) (stop [this context] (swap! lifecycle-events conj :stop) context)) config-fn (constantly {}) app (internal/build-app* [svc] config-fn) main-thread (future (internal/boot-services-for-app* app))] (while (< (count @lifecycle-events) 1) (Thread/yield)) (is (= [:init] @lifecycle-events)) (is (not (realized? main-thread))) (let [restart1-scheduled (promise) restart1-thread (future (internal/restart-tk-apps [app]) (deliver restart1-scheduled true)) restart2-scheduled (promise) restart2-thread (future (internal/restart-tk-apps [app]) (deliver restart2-scheduled true))] @restart1-scheduled (is (= [:init] @lifecycle-events)) @restart1-thread @restart2-scheduled (is (= [:init] @lifecycle-events)) @restart2-thread) (deliver boot-promise true) @main-thread (while (< (count @lifecycle-events) 8) (Thread/yield)) (is (= [:init :start :stop :init :start :stop :init :start] @lifecycle-events)) (tk-app/stop app) (is (= [:init :start :stop :init :start :stop :init :start :stop] @lifecycle-events))))) (deftest test-max-queued-restarts (let [stop-promise (promise) lifecycle-events (atom []) svc (tk/service [] (init [this context] (swap! lifecycle-events conj :init) context) (start [this context] (swap! lifecycle-events conj :start) context) (stop [this context] @stop-promise (swap! lifecycle-events conj :stop) context)) app (testutils/bootstrap-services-with-config [svc] {})] ;; the first restart will be picked up by the async worker, but it will ;; block on the 'stop-promise', so no more work can be picked up off of the ;; queue (internal/restart-tk-apps [app]) ;; now we issue how ever many restarts we need to to fill up the queue (dotimes [_i internal/max-pending-lifecycle-events] (internal/restart-tk-apps [app])) ;; now we choose some arbitrary number of additional restarts to request, ;; and confirm that we get a log message indicating that they were rejected (dotimes [_i 3] (logging/with-test-logging (internal/restart-tk-apps [app]) (is (logged? (format "Ignoring new SIGHUP restart requests; too many requests queued (%s)" internal/max-pending-lifecycle-events) :warn) "Missing expected log message when too many HUP requests queued"))) ;; now we unblock all of the queued restarts (deliver stop-promise true) ;; and validate that the life cycle events match up to that number of restarts (let [expected-lifecycle-events (->> [:stop :init :start] ; each restart will add these (repeat (+ 1 internal/max-pending-lifecycle-events)) (apply concat) (concat [:init :start]) ; here is the initial init/start vec)] (while (< (count @lifecycle-events) (count expected-lifecycle-events)) (Thread/yield)) (is (= expected-lifecycle-events @lifecycle-events)) ;; now we stop the app (tk-app/stop app) ;; and make sure that we got one last :stop (is (= (conj expected-lifecycle-events :stop) @lifecycle-events)))))puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/logging_test.clj000066400000000000000000000112671463756611100302750ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.logging-test (:require [clojure.java.io :as io] clojure.stacktrace [clojure.test :refer :all] [clojure.tools.logging :as log] [puppetlabs.trapperkeeper.logging :as tk-logging] [puppetlabs.trapperkeeper.testutils.logging :refer :all] [schema.test :as schema-test]) (:import (ch.qos.logback.classic Level))) (use-fixtures :each reset-logging-config-after-test schema-test/validate-schemas) (deftest test-catch-all-logger (testing "catch-all-logger ensures that message from an exception is logged" (with-test-logging ;; Prevent the stacktrace from being printed out (with-redefs [clojure.stacktrace/print-cause-trace (fn [_e] nil)] (tk-logging/catch-all-logger (Exception. "This exception is expected; testing error logging") "this is my error message")) (is (logged? #"this is my error message" :error))))) (deftest with-test-logging-on-separate-thread (testing "test-logging captures log messages from `future` threads" (with-test-logging (let [log-future (future (log/error "yo yo yo"))] @log-future (is (logged? #"yo yo yo" :error))))) (testing "threading doesn't break stuff" (with-test-logging (let [done? (promise)] (.start (Thread. (fn [] (log/info "test thread") (deliver done? true)))) (is (true? @done?)) (is (logged? #"test thread" :info)))))) (deftest with-test-logging-and-duplicate-log-lines (testing "test-logging captures matches duplicate lines when specified" (with-test-logging (log/error "duplicate message") (log/error "duplicate message") (log/warn "duplicate message") (log/warn "single message") (testing "single line only match" (is (not (logged? #"duplicate message"))) ;; original behavior of the fn, default behavior (is (logged? #"duplicate message" :warn false))) (testing "disabling single line match, enabling multiple line match" (is (logged? #"duplicate message" :error true)) (is (logged? #"duplicate message" nil true)) (testing "still handles single matches" (is (logged? #"single message" nil true)) (is (logged? #"single message" :warn true))))))) (deftest test-logging-configuration (testing "Calling `configure-logging!` with a logback.xml file" (tk-logging/configure-logging! "./dev-resources/logging/logback-debug.xml") (is (= (Level/DEBUG) (.getLevel (tk-logging/root-logger))))) (testing "Calling `configure-logging!` with another logback.xml file in case the default logging level is DEBUG" (tk-logging/configure-logging! "./dev-resources/logging/logback-warn.xml") (is (= (Level/WARN) (.getLevel (tk-logging/root-logger))))) (testing "a logging config file isn't required" ;; This looks strange, but we're trying to make sure that there are ;; no exceptions thrown when we configure logging without a log config file. (is (= nil (tk-logging/configure-logging! nil)))) (testing "support for logback evaluator filters" ;; This logging config file configures some fancy logback EvaluatorFilters, ;; and writes the log output to a file in `target/test`. (tk-logging/configure-logging! "./dev-resources/logging/logback-evaluator-filter.xml") (log/info "Hi! I should get filtered.") (log/info "Hi! I shouldn't get filtered.") (log/info (IllegalStateException. "OMGOMG") "Hi! I have an exception that should get filtered.") (with-open [reader (io/reader "./target/test/logback-evaluator-filter-test.log")] (let [lines (line-seq reader)] (is (= 1 (count lines))) (is (re-matches #".*Hi! I shouldn't get filtered\..*" (first lines))))))) (deftest test-logs-matching (let [log-lines '([puppetlabs.trapperkeeper.logging-test :info nil "log message1 at info"] [puppetlabs.trapperkeeper.logging-test :debug nil "log message1 at debug"] [puppetlabs.trapperkeeper.logging-test :warn nil "log message2 at warn"])] (testing "logs-matching can filter on message" ;; ignore deprecations #_:clj-kondo/ignore (is (= 2 (count (logs-matching #"log message1" log-lines))))) (testing "logs-matching can filter on message and level" ;; ignore deprecations #_:clj-kondo/ignore (is (= 1 (count (logs-matching #"log message1" log-lines :debug)))) #_:clj-kondo/ignore (is (= "log message1 at debug" (-> (logs-matching #"log message1" log-lines :debug) first :message))) #_:clj-kondo/ignore (is (empty? (logs-matching #"log message2" log-lines :info)))))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/optional_deps_test.clj000066400000000000000000000246161463756611100315110ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.optional-deps-test (:require [clojure.test :refer :all] [puppetlabs.trapperkeeper.services :refer [service defservice get-service] :as tks] [puppetlabs.trapperkeeper.app :as tka] [puppetlabs.trapperkeeper.core :refer [build-app] :as tkc] [schema.test :as schema-test])) (use-fixtures :once schema-test/validate-schemas) (defprotocol HaikuService (haiku [this topic])) (defprotocol SonnetService (sonnet [this topic couplet])) (defprotocol PoetryService (get-haiku [this]) (get-sonnet [this])) (defservice haiku-service HaikuService [] (haiku [this topic] ["here is a haiku" "about the topic you want" topic])) (defservice sonnet-service SonnetService {:required [] :optional []} (sonnet [this topic couplet] (vec (concat ["imagine a sonnet" (format "about %s" topic)] couplet)))) (deftest optional-deps-test (testing "when using defservice" (testing "with destructuring" (testing "and protocol" (defservice poetry-svc-w-destructure-and-proto PoetryService {:required [[:HaikuService haiku]] :optional [SonnetService]} (init [this ctx] ctx) (get-haiku [this] (haiku "meh")) (get-sonnet [this] "feh")) (is (build-app [poetry-svc-w-destructure-and-proto haiku-service] {}))) (testing "and no protocol" (defservice poetry-svc-w-destructure {:required [[:HaikuService haiku]] :optional [SonnetService]} (init [this ctx] ctx)) (is (build-app [poetry-svc-w-destructure haiku-service] {})))) (testing "with a protocol" (defservice poetry-svc-w-protocol PoetryService {:required [HaikuService] :optional [SonnetService]} (init [this ctx] ctx) (get-haiku [this] "haiku") (get-sonnet [this] "sonnet")) (is (build-app [poetry-svc-w-protocol haiku-service] {}))) (testing "without a protocol" (defservice poetry-svc-wo-protocol {:required [HaikuService] :optional [SonnetService]} (init [this ctx] ctx)) (is (build-app [poetry-svc-wo-protocol haiku-service] {}))) (testing "with metadata" (testing "and a protocol" (defservice poetry-svc-w-meta-and-proto {:private true} PoetryService {:required [HaikuService] :optional [SonnetService]} (init [this ctx] ctx) (get-haiku [this] "haiku") (get-sonnet [this] "sonnet")) (is (build-app [poetry-svc-w-meta-and-proto haiku-service] {}))) (testing "and no protocol" (defservice poetry-svc-w-meta-and-no-proto {:private true} {:required [HaikuService] :optional [SonnetService]} (init [this ctx] ctx)) (is (build-app [poetry-svc-w-meta-and-no-proto haiku-service] {})))) (testing "with a docstring" (testing "and a protocol" (defservice poetry-svc-w-doc-and-proto "foo bar butt" PoetryService {:required [HaikuService] :optional [SonnetService]} (init [this ctx] ctx) (get-haiku [this] "haiku") (get-sonnet [this] "sonnet")) (is (build-app [poetry-svc-w-doc-and-proto haiku-service] {}))) (testing "and no protocol" (defservice poetry-svc-w-doc-and-no-proto "fop blop bork" {:required [HaikuService] :optional [SonnetService]} (init [this ctx] ctx)) (is (build-app [poetry-svc-w-doc-and-no-proto haiku-service] {})))) (testing "with docstring and metadata" (testing "and a protocol" (defservice poetry-svc-w-everything "flarp" {:private true} PoetryService {:required [HaikuService] :optional [SonnetService]} (init [this ctx] ctx) (get-haiku [this] "haiku") (get-sonnet [this] "sonnet")) (is (build-app [poetry-svc-w-everything haiku-service] {}))) (testing "and no protocol" (defservice poetry-svc-w-everything-but-proto "fop blop bork" {:required [HaikuService] :optional [SonnetService]} (init [this ctx] ctx)) (is (build-app [poetry-svc-w-everything-but-proto haiku-service] {}))))) (testing "when not using a protocol" (let [poetry-service (service {:required [HaikuService] :optional [SonnetService]} (init [this ctx] (assoc ctx :haiku-svc (get-service this :HaikuService) :sonnet-svc (tks/maybe-get-service this :SonnetService))))] (is (build-app [poetry-service haiku-service] {})))) (testing "when dep form is well formed" (testing "when there are no optional deps" (let [poetry-service (service PoetryService {:required [HaikuService SonnetService] :optional []} (get-haiku [this] (let [haiku-svc (get-service this :HaikuService)] (haiku haiku-svc "tea leaves thwart those who"))) (get-sonnet [this] (let [sonnet-svc (get-service this :SonnetService)] (sonnet sonnet-svc "designing futures" ["rhyming" "is overrated"])))) app (build-app [haiku-service poetry-service sonnet-service] {})] (is (= ["here is a haiku" "about the topic you want" "tea leaves thwart those who"] (get-haiku (tka/get-service app :PoetryService)))) (is (= ["imagine a sonnet" "about designing futures" "rhyming" "is overrated"] (get-sonnet (tka/get-service app :PoetryService)))))) (testing "when there are normal optional deps" (testing "and they are all included" (let [poetry-service (service PoetryService {:required [] :optional [HaikuService SonnetService]} (get-haiku [this] (let [haiku-svc (get-service this :HaikuService)] (haiku haiku-svc "tea leaves thwart those who"))) (get-sonnet [this] (let [sonnet-svc (get-service this :SonnetService)] (sonnet sonnet-svc "designing futures" ["rhyming" "is overrated"])))) app (build-app [haiku-service poetry-service sonnet-service] {})] (is (= ["here is a haiku" "about the topic you want" "tea leaves thwart those who"] (get-haiku (tka/get-service app :PoetryService)))) (is (= ["imagine a sonnet" "about designing futures" "rhyming" "is overrated"] (get-sonnet (tka/get-service app :PoetryService)))))) (testing "and one is excluded" (let [poetry-service (service PoetryService {:required [] :optional [HaikuService SonnetService]} (get-haiku [this] (let [haiku-svc (get-service this :HaikuService)] (haiku haiku-svc "tea leaves thwart those who"))) (get-sonnet [this] (if (tks/service-included? this :SonnetService) (sonnet (get-service this :SonnetService) "designing futures" ["rhyming" "is overrated"]) ["imagine the saddest sonnet"]))) app (build-app [haiku-service poetry-service] {})] (is (= ["here is a haiku" "about the topic you want" "tea leaves thwart those who"] (get-haiku (tka/get-service app :PoetryService)))) (is (= ["imagine the saddest sonnet"] (get-sonnet (tka/get-service app :PoetryService)))))) (testing "and all are excluded" (let [poetry-service (service PoetryService {:required [] :optional [HaikuService SonnetService]} (get-haiku [this] (if-let [haiku-svc (tks/maybe-get-service this :HaikuService)] (haiku haiku-svc ["tea leaves thwart those who"]) ["imagine the saddest haiku"])) (get-sonnet [this] (if (tks/service-included? this :SonnetService) (sonnet (get-service this :SonnetService) "designing futures" ["rhyming" "is overrated"]) ["imagine the saddest sonnet"]))) app (build-app [poetry-service] {})] (is (= ["imagine the saddest haiku"] (get-haiku (tka/get-service app :PoetryService)))) (is (= ["imagine the saddest sonnet"] (get-sonnet (tka/get-service app :PoetryService))))))) (testing "when there is a destructured required dep" (let [poetry-service (service PoetryService {:required [[:SonnetService sonnet]] :optional [HaikuService]} (get-haiku [this] (if-let [haiku-svc (tks/maybe-get-service this :HaikuService)] (haiku haiku-svc ["tea leaves thwart those who"]) ["imagine the saddest haiku"])) (get-sonnet [this] (sonnet "designing futures" ["rhyming" "is overrated"]))) app (build-app [poetry-service sonnet-service] {})] (is (= ["imagine the saddest haiku"] (get-haiku (tka/get-service app :PoetryService)))) (is (= ["imagine a sonnet" "about designing futures" "rhyming" "is overrated"] (get-sonnet (tka/get-service app :PoetryService)))))))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/plugins_test.clj000066400000000000000000000042141463756611100303220ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.plugins-test (:require [clojure.java.io :refer [file resource]] [clojure.test :refer :all] [puppetlabs.trapperkeeper.app :refer [service-graph]] [puppetlabs.trapperkeeper.plugins :as plugins] [puppetlabs.trapperkeeper.testutils.bootstrap :refer [bootstrap-with-empty-config]] [schema.test :as schema-test])) (use-fixtures :once schema-test/validate-schemas) (deftest test-jars-in-dir (let [jars (plugins/jars-in-dir (file "plugin-test-resources/plugins"))] (is (= 1 (count jars))) (is (= "plugin-test-resources/plugins/test-service.jar" (.getPath (first jars)))))) (deftest test-bad-directory (testing "TK throws an exception if --plugins is provided with a dir that does not exist." (is (thrown-with-msg? IllegalArgumentException #".*directory.*does not exist.*" (bootstrap-with-empty-config ["--plugins" "/this/does/not/exist"]))))) (deftest test-no-duplicates (testing "duplicate test passes on .jar with just a service in it" ;; `verify-no-duplicate-resources` throws an exception if a duplicate is found. (plugins/verify-no-duplicate-resources (file "plugin-test-resources/plugins/test-service.jar")))) (deftest test-duplicates (testing "duplicate test fails when an older version of kitchensink is included" (is (thrown-with-msg? IllegalArgumentException #".*Class or namespace.*found in both.*" (plugins/verify-no-duplicate-resources (file "plugin-test-resources/bad-plugins")))))) (deftest test-plugin-service (testing "TK can load and use service defined in plugin .jar" (let [app (bootstrap-with-empty-config ["--plugins" "./plugin-test-resources/plugins" "--bootstrap-config" "./dev-resources/bootstrapping/plugin/bootstrap.cfg"]) service-fn (-> (service-graph app) :PluginTestService :moo)] (is (= "This message comes from the plugin test service." (service-fn))) ;; Can it also load resources from that jar (is (resource "test_services/plugin_test_services.clj"))))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/services/000077500000000000000000000000001463756611100267325ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/services/config/000077500000000000000000000000001463756611100301775ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/services/config/typesafe_test.clj000066400000000000000000000024501463756611100335510ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.services.config.typesafe-test (:require [puppetlabs.config.typesafe :as ts] [clojure.test :refer :all] [schema.test :as schema-test])) (use-fixtures :once schema-test/validate-schemas) (deftest configfile->map-test (testing "can parse .properties file with nested data structures" (let [cfg (ts/config-file->map "./dev-resources/config/file/config.properties")] (is (= {:foo {:bar "barbar" :baz "bazbaz" :bam 42 :bap {:boozle "boozleboozle"}}} cfg)))) (testing "can parse .json file with nested data structures" (let [cfg (ts/config-file->map "./dev-resources/config/file/config.json")] (is (= {:foo {:bar "barbar" :baz "bazbaz" :bam 42 :bap {:boozle "boozleboozle" :bip [1 2 {:hi "there"} 3]}}} cfg)))) (testing "can parse .conf file with nested data structures" (let [cfg (ts/config-file->map "./dev-resources/config/file/config.conf")] (is (= {:foo {:bar "barbar" :baz "bazbaz" :bam 42 :bap {:boozle "boozleboozle" :bip [1 2 {:hi "there"} 3]}}} cfg)))))puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/services/nrepl/000077500000000000000000000000001463756611100300525ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/services/nrepl/nrepl_service_test.clj000066400000000000000000000057451463756611100344560ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.services.nrepl.nrepl-service-test (:require [clojure.test :refer :all] [nrepl.core :as repl] [puppetlabs.trapperkeeper.testutils.bootstrap :refer [with-app-with-config]] [puppetlabs.trapperkeeper.services.nrepl.nrepl-service :as nrepl-service] [schema.test :as schema-test])) (use-fixtures :once schema-test/validate-schemas) (deftest test-nrepl-config (letfn [(process-config-fn [enabled] (->> {:nrepl {:enabled enabled}} (partial get-in) nrepl-service/process-config :enabled?))] (testing "Should support string value for `enabled?`" (is (= true (process-config-fn "true"))) (is (= false (process-config-fn "false")))) (testing "Should support boolean value for `enabled?`" (is (= true (process-config-fn true))) (is (= false (process-config-fn false)))))) (deftest test-nrepl-service (testing "An nREPL service has been started" (with-app-with-config app [nrepl-service/nrepl-service] {:nrepl {:port 7888 :host "0.0.0.0" :enabled "true"}} (is (= [2] (with-open [conn (repl/connect :port 7888)] (-> (repl/client conn 1000) (repl/message {:op "eval" :code "(+ 1 1)"}) (repl/response-values)))))))) (deftest test-nrepl-service-2 (testing "An nREPL service without middlewares has been started" (with-app-with-config app [nrepl-service/nrepl-service] {:nrepl {:port 7888 :host "0.0.0.0" :enabled "true" :middlewares []}} (is (= [2] (with-open [conn (repl/connect :port 7888)] (-> (repl/client conn 1000) (repl/message {:op "eval" :code "(+ 1 1)"}) (repl/response-values)))))))) (deftest test-nrepl-service-3 (testing "An nREPL service with test middleware has been started" (with-app-with-config app [nrepl-service/nrepl-service] {:nrepl {:port 7888 :host "0.0.0.0" :enabled "true" :middlewares "[puppetlabs.trapperkeeper.services.nrepl.nrepl-test-send-middleware/send-test]"}} (is (= "success" (with-open [conn (repl/connect :port 7888)] (:test (first (-> (repl/client conn 1000) (repl/message {:op "middlewaretest"})))))))) (with-app-with-config app [nrepl-service/nrepl-service] {:nrepl {:port 7888 :host "0.0.0.0" :enabled "true" :middlewares ["puppetlabs.trapperkeeper.services.nrepl.nrepl-test-send-middleware/send-test"]}} (is (= "success" (with-open [conn (repl/connect :port 7888)] (:test (first (-> (repl/client conn 1000) (repl/message {:op "middlewaretest"})))))))))) nrepl_test_send_middleware.clj000066400000000000000000000007161463756611100360560ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/services/nrepl(ns puppetlabs.trapperkeeper.services.nrepl.nrepl-test-send-middleware (:require [nrepl.transport :as t] [nrepl.middleware :refer [set-descriptor!]] [nrepl.misc :refer [response-for]])) (defn send-test [h] (fn [{:keys [op transport] :as msg}] (if (= "middlewaretest" op) (t/send transport (response-for msg :status "done" :test "success")) (h msg)))) (set-descriptor! #'send-test {:requires #{} :expects #{}}) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/services_internal_test.clj000066400000000000000000000202651463756611100323640ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.services-internal-test (:import (clojure.lang IFn)) (:require [clojure.test :refer :all] [plumbing.fnk.pfnk :as pfnk] [schema.core :as schema] [schema.test :as schema-test] [puppetlabs.trapperkeeper.app :as app] [puppetlabs.trapperkeeper.services :refer [service service-map] :as svcs] [puppetlabs.trapperkeeper.services-internal :as si] [puppetlabs.trapperkeeper.testutils.bootstrap :refer [with-app-with-empty-config]])) (use-fixtures :once schema-test/validate-schemas) (deftest service-forms-test (testing "should support forms that include protocol" (is (= {:dependencies [] :fns '() :service-protocol-sym 'Foo} (si/find-prot-and-deps-forms! '(Foo []))))) (testing "should support forms that do not include protocol" (is (= {:dependencies [] :fns '() :service-protocol-sym nil} (si/find-prot-and-deps-forms! '([]))))) (testing "result should include vector of fn forms if provided" (is (= {:dependencies [] :fns '((fn1 [] "fn1") (fn2 [] "fn2")) :service-protocol-sym 'Foo} (si/find-prot-and-deps-forms! '(Foo [] (fn1 [] "fn1") (fn2 [] "fn2"))))) (is (= {:dependencies [] :fns '((fn1 [] "fn1") (fn2 [] "fn2")) :service-protocol-sym nil} (si/find-prot-and-deps-forms! '([] (fn1 [] "fn1") (fn2 [] "fn2")))))) (testing "should throw exception if the first form is not the protocol symbol or dependency vector" (is (thrown-with-msg? IllegalArgumentException #"Invalid service definition; first form must be protocol or dependency list; found '\"hi\"'" (si/find-prot-and-deps-forms! '("hi" []))))) (testing "should throw exception if the first form is a protocol sym and the second is not a dependency vector" (is (thrown-with-msg? IllegalArgumentException #"Invalid service definition; expected dependency list following protocol, found: '\"hi\"'" (si/find-prot-and-deps-forms! '(Foo "hi"))))) (testing "should throw an exception if all remaining forms are not seqs" (is (thrown-with-msg? IllegalArgumentException #"Invalid service definition; expected function definitions following dependency list, invalid value: '\"hi\"'" (si/find-prot-and-deps-forms! '(Foo [] (fn1 [] "fn1") "hi")))))) (defn local-resolve "Resolve symbol in current (services-internal-test) namespace" [sym] {:pre [(symbol? sym)]} (ns-resolve 'puppetlabs.trapperkeeper.services-internal-test sym)) (defprotocol EmptyProtocol) (def NonProtocolSym "hi") (deftest protocol-syms-test (testing "should not throw exception if protocol exists" (is (si/protocol? (si/validate-protocol-sym! 'EmptyProtocol (local-resolve 'EmptyProtocol))))) (testing "should throw exception if service protocol sym is not resolvable" (is (thrown-with-msg? IllegalArgumentException #"Unrecognized service protocol 'UndefinedSym'" (si/validate-protocol-sym! 'UndefinedSym (local-resolve 'UndefinedSym))))) (testing "should throw exception if service protocol symbol is resolveable but does not resolve to a protocol" (is (thrown-with-msg? IllegalArgumentException #"Specified service protocol 'NonProtocolSym' does not appear to be a protocol!" (si/validate-protocol-sym! 'NonProtocolSym (local-resolve 'NonProtocolSym)))))) (deftest build-fns-map-test (testing "minimal services may not define functions other than lifecycle functions" (is (thrown-with-msg? IllegalArgumentException #"Service attempts to define function 'foo', but does not provide protocol" (si/build-fns-map! nil [] ['init 'start] '((init [this context] context) (start [this context] context) (foo [this] "foo"))))))) (defprotocol Service1 (service1-fn [this])) (defprotocol Service2 (service2-fn [this])) (defprotocol BadServiceProtocol (start [this])) (deftest invalid-fns-test (testing "should throw an exception if there is no definition of a function in the protocol" (is (thrown-with-msg? IllegalArgumentException #"Service does not define function 'service1-fn', which is required by protocol 'Service1'" (si/parse-service-forms! ['init 'start] (cons 'puppetlabs.trapperkeeper.services-internal-test/Service1 '([] (init [this context] context))))))) (testing "should throw an exception if there is a definition for a function that is not in the protocol" (is (thrown-with-msg? IllegalArgumentException #"Service attempts to define function 'foo', which does not exist in protocol 'Service1'" (si/parse-service-forms! ['init 'start] (cons 'puppetlabs.trapperkeeper.services-internal-test/Service1 '([] (foo [this] "foo"))))))) (testing "should throw an exception if the protocol includes a function with the same name as a lifecycle function" (is (thrown-with-msg? IllegalArgumentException #"Service protocol 'BadServiceProtocol' includes function named 'start', which conflicts with lifecycle function by same name" (si/parse-service-forms! ['init 'start] (cons 'puppetlabs.trapperkeeper.services-internal-test/BadServiceProtocol '([] (start [this] "foo")))))))) (deftest prismatic-functionality-test (testing "prismatic fnk is initialized properly" (let [service1 (service Service1 [] (init [this context] context) (start [this context] context) (service1-fn [this] "Foo!")) service2 (service Service2 [[:Service1 service1-fn]] (init [this context] context) (start [this context] context) (service2-fn [this] "Bar!")) s1-graph (service-map service1) s2-graph (service-map service2)] (is (map? s1-graph)) (let [graph-keys (keys s1-graph)] (is (= (count graph-keys) 1)) (is (= (first graph-keys) :Service1))) (let [service-fnk (:Service1 s1-graph) depends (pfnk/input-schema service-fnk) provides (pfnk/output-schema service-fnk)] (is (ifn? service-fnk)) (is (= depends {schema/Keyword schema/Any :tk-app-context schema/Any :tk-service-refs schema/Any})) (is (= provides {:service1-fn IFn}))) (is (map? s2-graph)) (let [graph-keys (keys s2-graph)] (is (= (count graph-keys) 1)) (is (= (first graph-keys) :Service2))) (let [service-fnk (:Service2 s2-graph) depends (pfnk/input-schema service-fnk) provides (pfnk/output-schema service-fnk) fnk-instance (service-fnk {:Service1 {:service1-fn identity} :tk-app-context (atom {}) :tk-service-refs (atom {})}) s2-fn (:service2-fn fnk-instance)] (is (ifn? service-fnk)) (is (= depends {schema/Keyword schema/Any :tk-app-context schema/Any :tk-service-refs schema/Any :Service1 {schema/Keyword schema/Any :service1-fn schema/Any}})) (is (= provides {:service2-fn IFn})) (is (= "Bar!" (s2-fn))))))) (defprotocol EmptyService) (deftest explicit-service-symbol-test (testing "can explicitly pass `service` a service symbol via internal API" (let [empty-service (service {:service-symbol foo/bar} EmptyService [])] (with-app-with-empty-config app [empty-service] (let [svc (app/get-service app :EmptyService)] (is (= :EmptyService (svcs/service-id svc))) (is (= (symbol "foo" "bar") (svcs/service-symbol svc))))))))puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/services_namespaces_test/000077500000000000000000000000001463756611100321705ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/services_namespaces_test/ns1.clj000066400000000000000000000001431463756611100333610ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.services-namespaces-test.ns1) (defprotocol FooService (foo [this]))puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/services_namespaces_test/ns2.clj000066400000000000000000000014271463756611100333700ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.services-namespaces-test.ns2 (:require [clojure.test :refer :all] [puppetlabs.kitchensink.testutils.fixtures :refer [with-no-jvm-shutdown-hooks]] [puppetlabs.trapperkeeper.core :as trapperkeeper] [puppetlabs.trapperkeeper.services-namespaces-test.ns1 :as ns1] [puppetlabs.trapperkeeper.testutils.bootstrap :refer [bootstrap-services-with-empty-config]] [schema.test :as schema-test])) (use-fixtures :once schema-test/validate-schemas with-no-jvm-shutdown-hooks) (trapperkeeper/defservice foo-service ns1/FooService [] (foo [this] "foo")) (deftest test-service-namespaces (testing "can boot service defined in different namespace than protocol" (bootstrap-services-with-empty-config [foo-service]) (is (true? true)))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/services_test.clj000066400000000000000000000623261463756611100304740ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.services-test (:require [clojure.test :refer :all] [me.raynes.fs :as fs] [puppetlabs.kitchensink.core :as kitchensink] [puppetlabs.kitchensink.testutils :as ks-testutils] [puppetlabs.kitchensink.testutils.fixtures :refer [with-no-jvm-shutdown-hooks]] [puppetlabs.trapperkeeper.app :as app] [puppetlabs.trapperkeeper.core :as tk] [puppetlabs.trapperkeeper.internal :as internal] [puppetlabs.trapperkeeper.services :refer [defservice service] :as svcs] [puppetlabs.trapperkeeper.testutils.bootstrap :refer [bootstrap-services-with-empty-config with-app-with-config with-app-with-empty-config]] [schema.test :as schema-test]) (:import (java.util.concurrent ExecutionException))) (use-fixtures :once schema-test/validate-schemas with-no-jvm-shutdown-hooks) (defprotocol EmptyService) (defprotocol HelloService (hello [this msg])) (defservice hello-service HelloService [] (init [this context] context) (start [this context] context) (hello [this msg] (str "HELLO!: " msg))) (deftest test-satisfies-protocols (testing "creates a service definition" (is (satisfies? svcs/ServiceDefinition hello-service))) (let [app (bootstrap-services-with-empty-config [hello-service])] (testing "app satisfies protocol" (is (satisfies? app/TrapperkeeperApp app))) (let [h-s (app/get-service app :HelloService)] (testing "service satisfies all protocols" (is (satisfies? svcs/Lifecycle h-s)) (is (satisfies? svcs/Service h-s)) (is (satisfies? HelloService h-s))) (testing "service functions behave as expected" (is (= "HELLO!: yo" (hello h-s "yo"))))))) (defprotocol Service1 (service1-fn [this])) (defprotocol Service2 (service2-fn [this])) (defprotocol Service3 (service3-fn [this])) (deftest test-services-not-required (testing "services are not required to define lifecycle functions" (let [service1 (service Service1 [] (service1-fn [this] "hi")) app (bootstrap-services-with-empty-config [service1])] (is (not (nil? app)))))) (deftest test-restart-file-correct-count (testing "create a restart file and check that it correctly increments when a service starts" (let [service1 (service Service1 [] (service1-fn [this] "hi")) temp-file (kitchensink/temp-file-name "counter")] (with-app-with-config app [service1] {:global {:restart-file temp-file}} (is (= (slurp temp-file) "1")))))) (deftest test-restart-file-HUP-restart (testing "check that restart file correctly increments on HUP restarts" (let [service1 (service Service1 [] (service1-fn [this] "hi")) temp-file (kitchensink/temp-file-name "counter")] (with-app-with-config app [service1] {:global {:restart-file temp-file}} (app/restart app) (is (= (slurp temp-file) "2")))))) (deftest test-restart-file-big-int-limit (testing "restart file should reset to 1 if the counter is too large" (let [service1 (service Service1 [] (service1-fn [this] "hi")) temp-file (kitchensink/temp-file-name "counter")] (spit temp-file "9223372036854775807") (with-app-with-config app [service1] {:global {:restart-file temp-file}} (is (= (slurp temp-file) "1"))) (with-app-with-config app [service1] {:global {:restart-file temp-file}} (is (= (slurp temp-file) "2")))))) (deftest test-invalid-restart-file (testing "restart file should reset to 1 if the file is unparseable" (let [service1 (service Service1 [] (service1-fn [this] "hi")) temp-file (kitchensink/temp-file-name "counter")] (spit temp-file "hello") (with-app-with-config app [service1] {:global {:restart-file temp-file}} (is (= (slurp temp-file) "1")))))) (deftest test-restart-file-no-permissions (testing "exception should be thrown if restart file is not readable/writeable" (let [service1 (service Service1 [] (service1-fn [this] "hi")) temp-file (kitchensink/temp-file-name "counter")] (fs/chmod "u-rw" (fs/touch temp-file)) (is (thrown? IllegalStateException (with-app-with-config app [service1] {:global {:restart-file temp-file}})))))) (deftest test-restart-file-missing-parent-dirs (testing "some portion of the parent directory structure does not exist" (let [service1 (service Service1 [] (service1-fn [this] "hi")) temp-file (fs/file (kitchensink/temp-dir "foo") "bar" "baz" "counter")] (is (false? (fs/exists? (fs/parent temp-file)))) (is (false? (fs/exists? temp-file))) (with-app-with-config app [service1] {:global {:restart-file temp-file}} (is (= (slurp temp-file) "1")))))) (defn create-lifecycle-services [call-seq] (let [lc-fn (fn [context action] (swap! call-seq conj action) context)] [(service Service1 [] (init [this context] (lc-fn context :init-service1)) (start [this context] (lc-fn context :start-service1)) (stop [this context] (lc-fn context :stop-service1)) (service1-fn [this] (lc-fn nil :service1-fn))) (service Service2 [[:Service1 service1-fn]] (init [this context] (lc-fn context :init-service2)) (start [this context] (lc-fn context :start-service2)) (stop [this context] (lc-fn context :stop-service2)) (service2-fn [this] (lc-fn nil :service2-fn))) (service Service3 [[:Service2 service2-fn]] (init [this context] (lc-fn context :init-service3)) (start [this context] (lc-fn context :start-service3)) (stop [this context] (lc-fn context :stop-service3)) (service3-fn [this] (lc-fn nil :service3-fn)))])) (deftest test-lifecycle-functions-ordered-correctly (testing "life cycle functions are called in the correct order" (let [call-seq (atom []) services (create-lifecycle-services call-seq)] (with-app-with-empty-config app services (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3] @call-seq))) (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3 :stop-service3 :stop-service2 :stop-service1] @call-seq)) (reset! call-seq []) (with-app-with-empty-config app services (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3] @call-seq))) (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3 :stop-service3 :stop-service2 :stop-service1] @call-seq))))) (deftest test-lifecycle-function-ordering-restart (testing "app restart calls life cycle functions in the correct order" (let [call-seq (atom []) services (create-lifecycle-services call-seq)] (with-app-with-empty-config app services (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3] @call-seq)) (app/restart app) (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3 :stop-service3 :stop-service2 :stop-service1 :init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3] @call-seq))) (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3 :stop-service3 :stop-service2 :stop-service1 :init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3 :stop-service3 :stop-service2 :stop-service1] @call-seq))))) (deftest test-lifecycle-function-ordering-signaling (testing "app restart calls life cycle functions in the correct order" (let [call-seq (atom []) services (create-lifecycle-services call-seq)] (with-app-with-empty-config app services (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3] @call-seq)) ;; It would be preferrable to send a HUP here, but jenkins clients ;; swallow the HUP signal, which makes the test fail. So instead we ;; just call the thing that the signal would have caused to be called. (internal/restart-tk-apps [app]) (let [start (System/currentTimeMillis)] (while (and (not= (count @call-seq) 15) (< (- (System/currentTimeMillis) start) 5000)) (Thread/yield))) (is (= (count @call-seq) 15)) (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3 :stop-service3 :stop-service2 :stop-service1 :init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3] @call-seq))) (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3 :stop-service3 :stop-service2 :stop-service1 :init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3 :stop-service3 :stop-service2 :stop-service1] @call-seq))))) (deftest test-context-cleared-on-restart (testing "service contexts should be cleared out during a restart" (let [test-init-context (atom nil) test-init-count (atom 0) test-start-context (atom nil) context-elem {:foo "bar"} service1 (service EmptyService [] (init [this context] (reset! test-init-context (merge context context-elem)) (swap! test-init-count inc) {:context @test-init-context :count @test-init-count}) (start [this context] (reset! test-start-context context)))] (with-app-with-empty-config app [service1] (is (= context-elem @test-init-context)) (is (= {:foo "bar"} (:context @test-start-context))) (is (= 1 (:count @test-start-context))) (swap! test-init-context dissoc :context) (swap! test-start-context dissoc :context) (app/restart app)) (is (= {:foo "bar"} @test-init-context)) (is (= {:foo "bar"} (:context @test-start-context))) (is (= 2 (:count @test-start-context)))))) (deftest test-exception-during-restart (testing "restart should halt if an exception is raised" (let [call-seq (atom []) services (conj (create-lifecycle-services call-seq) (service EmptyService [] (stop [this context] (throw (IllegalStateException. "Exploding Service")))))] (ks-testutils/with-no-jvm-shutdown-hooks ; We can't use the with-app-with-empty-config macro because we don't ; want to use its implicit tk-app/stop call. We're asserting that ; the stop will happen because of the exception. So instead, we use ; the tk/run-app here to block on the app until the restart is ; called and explodes in an exception. (let [app (internal/throw-app-error-if-exists! (bootstrap-services-with-empty-config services)) app-running (future (tk/run-app app))] (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3] @call-seq)) (app/restart app) (try @app-running (catch ExecutionException e (is (instance? IllegalStateException (.getCause e))) (is (= "Exploding Service" (.. e getCause getMessage))))))) ; Here we validate that the stop completed but no new init happened (is (= [:init-service1 :init-service2 :init-service3 :start-service1 :start-service2 :start-service3 :stop-service3 :stop-service2 :stop-service1] @call-seq))))) (deftest test-lifecycle-service-id-available (testing "service-id should be able to be called from any lifecycle phase" (let [test-context (atom {}) service1 (service Service1 [] (init [this context] (swap! test-context assoc :init-service-id (svcs/service-id this)) context) (start [this context] (swap! test-context assoc :start-service-id (svcs/service-id this)) context) (stop [this context] (swap! test-context assoc :stop-service-id (svcs/service-id this)) context) (service1-fn [this] nil))] (with-app-with-empty-config app [service1] ;; no-op; we just want the app to start up and shut down ) (is (= :Service1 (:init-service-id @test-context))) (is (= :Service1 (:start-service-id @test-context))) (is (= :Service1 (:stop-service-id @test-context)))))) (deftest dependencies-test (testing "services should be able to call functions in dependency list" (let [service1 (service Service1 [] (service1-fn [this] "FOO!")) service2 (service Service2 [[:Service1 service1-fn]] (service2-fn [this] (str "HELLO " (service1-fn)))) app (bootstrap-services-with-empty-config [service1 service2]) s2 (app/get-service app :Service2)] (is (= "HELLO FOO!" (service2-fn s2))))) (testing "services should be able to retrieve instances of services that they depend on" (let [service1 (service Service1 [] (service1-fn [this] "FOO!")) service2 (service Service2 [[:Service1 service1-fn]] (init [this context] (let [s1 (svcs/get-service this :Service1)] (assoc context :s1 s1))) (service2-fn [this] ((svcs/service-context this) :s1))) app (bootstrap-services-with-empty-config [service1 service2]) s2 (app/get-service app :Service2) s1 (service2-fn s2)] (is (satisfies? Service1 s1)) (is (= "FOO!" (service1-fn s1))))) (testing "an error should be thrown if calling get-service on a non-existent service" (let [service1 (service Service1 [] (service1-fn [this] (svcs/get-service this :NonExistent))) app (bootstrap-services-with-empty-config [service1]) s1 (app/get-service app :Service1)] (is (thrown-with-msg? IllegalArgumentException #"Call to 'get-service' failed; service ':NonExistent' does not exist." (service1-fn s1))))) (testing "lifecycle functions should be able to call injected functions" (let [service1 (service Service1 [] (service1-fn [this] "FOO!")) service2 (service Service2 [[:Service1 service1-fn]] (init [this context] (assoc context :injected-fn-result (service1-fn))) (service2-fn [this] ((svcs/service-context this) :injected-fn-result))) app (bootstrap-services-with-empty-config [service1 service2]) s2 (app/get-service app :Service2)] (is (= "FOO!" (service2-fn s2)))))) (defprotocol Service4 (service4-fn1 [this]) (service4-fn2 [this])) (deftest service-this-test (testing "should be able to call other functions in same service via 'this'" (let [service4 (service Service4 [] (service4-fn1 [this] "foo!") (service4-fn2 [this] (str (service4-fn1 this) " bar!"))) app (bootstrap-services-with-empty-config [service4]) s4 (app/get-service app :Service4)] (is (= "foo! bar!" (service4-fn2 s4)))))) (defservice service1 Service1 [] (init [this context] "hi") (service1-fn [this] "hi")) (defservice service1-alt Service1 [] (start [this context] "hi") (service1-fn [this] "hi")) (deftest context-test (testing "should error if lifecycle function doesn't return context" (is (thrown-with-msg? IllegalStateException (re-pattern (str "Lifecycle function 'init' for service " "'puppetlabs.trapperkeeper.services-test/service1'" " must return a context map \\(got: \"hi\"\\)")) (bootstrap-services-with-empty-config [service1])) "Unexpected shutdown reason for bootstrap") (is (thrown-with-msg? IllegalStateException (re-pattern (str "Lifecycle function 'start' for service " "'puppetlabs.trapperkeeper.services-test/service1-alt'" " must return a context map " "\\(got: \"hi\"\\)")) (bootstrap-services-with-empty-config [service1-alt])) "Unexpected shutdown reason for bootstrap")) (testing "lifecycle error works if service has no service symbol" (let [service1 (service Service1 [] (init [this context] "hi") (service1-fn [this] "hi"))] (is (thrown-with-msg? IllegalStateException (re-pattern (str "Lifecycle function 'init' for service ':Service1'" " must return a context map \\(got: \"hi\"\\)")) (bootstrap-services-with-empty-config [service1])) "Unexpected shutdown reason for bootstrap")) (let [service1 (service Service1 [] (start [this context] "hi") (service1-fn [this] "hi"))] (is (thrown-with-msg? IllegalStateException (re-pattern (str "Lifecycle function 'start' for service " "':Service1' must return a context map " "\\(got: \"hi\"\\)")) (bootstrap-services-with-empty-config [service1])) "Unexpected shutdown reason for bootstrap"))) (testing "context should be available in subsequent lifecycle functions" (let [start-context (atom nil) service1 (service Service1 [] (init [this context] (assoc context :foo :bar)) (start [this context] (reset! start-context context)) (service1-fn [this] "hi"))] (bootstrap-services-with-empty-config [service1]) (is (= {:foo :bar} @start-context)))) (testing "context should be accessible in service functions" (let [sfn-context (atom nil) service1 (service Service1 [] (init [this context] (assoc context :foo :bar)) (service1-fn [this] (reset! sfn-context (svcs/service-context this)))) app (bootstrap-services-with-empty-config [service1]) s1 (app/get-service app :Service1)] (service1-fn s1) (is (= {:foo :bar} @sfn-context)) (is (= {:foo :bar} (svcs/service-context s1))))) (testing "context works correctly in injected functions" (let [service1 (service Service1 [] (init [this context] (assoc context :foo :bar)) (service1-fn [this] ((svcs/service-context this) :foo))) service2 (service Service2 [[:Service1 service1-fn]] (service2-fn [this] (service1-fn))) app (bootstrap-services-with-empty-config [service1 service2]) s2 (app/get-service app :Service2)] (is (= :bar (service2-fn s2))))) (testing "context works correctly in service functions called by other functions in same service" (let [service4 (service Service4 [] (init [this context] (assoc context :foo :bar)) (service4-fn1 [this] ((svcs/service-context this) :foo)) (service4-fn2 [this] (service4-fn1 this))) app (bootstrap-services-with-empty-config [service4]) s4 (app/get-service app :Service4)] (is (= :bar (service4-fn2 s4))))) (testing "context from other services should not be visible" (let [s2-context (atom nil) service1 (service Service1 [] (init [this context] (assoc context :foo :bar)) (service1-fn [this] "hi")) service2 (service Service2 [[:Service1 service1-fn]] (start [this context] (reset! s2-context (svcs/service-context this))) (service2-fn [this] "hi")) _app (bootstrap-services-with-empty-config [service1 service2])] (is (= {} @s2-context))))) (deftest service-symbol-test (testing "service defined via `defservice` has a service symbol" (with-app-with-empty-config app [hello-service] (let [svc (app/get-service app :HelloService)] (is (= (symbol "puppetlabs.trapperkeeper.services-test" "hello-service") (svcs/service-symbol svc)))))) (testing "service defined via `service` does not have a service symbol" (let [empty-svc (service EmptyService [])] (with-app-with-empty-config app [empty-svc] (let [svc (app/get-service app :EmptyService)] (is (= :EmptyService (svcs/service-id svc))) (is (nil? (svcs/service-symbol svc)))))))) (deftest get-services-test (testing "get-services should return all services" (let [empty-service (service EmptyService [])] (with-app-with-empty-config app [empty-service hello-service] (let [empty (app/get-service app :EmptyService) hello (app/get-service app :HelloService)] (doseq [s [empty hello]] (let [all-services (svcs/get-services s)] (is (= 2 (count all-services))) (is (every? #(satisfies? svcs/Service %) all-services)) (is (= #{:EmptyService :HelloService} (set (map svcs/service-id all-services))))))))))) (deftest minimal-services-test (testing "minimal services can be defined without a protocol" (let [call-seq (atom []) service0 (service [] (init [this context] (swap! call-seq conj :init) (assoc context :foo :bar)) (start [this context] (swap! call-seq conj :start) (is (= context {:foo :bar})) context))] (bootstrap-services-with-empty-config [service0]) (is (= [:init :start] @call-seq)))) (testing "minimal services can have dependencies" (let [service1 (service Service1 [] (service1-fn [this] "hi")) result (atom nil) service0 (service [[:Service1 service1-fn]] (init [this context] (reset! result (service1-fn)) context))] (bootstrap-services-with-empty-config [service1 service0]) (is (= "hi" @result))))) (defprotocol MultiArityService (foo [this x] [this x y])) (deftest test-multi-arity-protocol-fn (testing "should support protocols with multi-arity fns" (let [ma-service (service MultiArityService [] (foo [this x] x) (foo [this x y] (+ x y))) service1 (service Service1 [[:MultiArityService foo]] (service1-fn [this] [(foo 5) (foo 3 6)])) app (bootstrap-services-with-empty-config [ma-service service1]) mas (app/get-service app :MultiArityService) s1 (app/get-service app :Service1)] (is (= 3 (foo mas 3))) (is (= 5 (foo mas 4 1))) (is (= [5 9] (service1-fn s1)))))) (deftest service-fn-invalid-docstring (testing "defining a service function, mistakenly adding a docstring" (try (macroexpand '(puppetlabs.trapperkeeper.services/service puppetlabs.trapperkeeper.services-test/Service1 [] (service1-fn "This is an example of an invalid docstring" [this] nil))) (catch Exception e (let [cause (-> e Throwable->map :cause)] (is (re-matches #"Incorrect macro usage.*" cause))))))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/shutdown_test.clj000066400000000000000000000606111463756611100305170ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.shutdown-test (:require [clojure.test :refer :all] [puppetlabs.kitchensink.testutils.fixtures :refer [with-no-jvm-shutdown-hooks]] [puppetlabs.trapperkeeper.app :as tk-app] [puppetlabs.trapperkeeper.core :as tk] [puppetlabs.trapperkeeper.internal :as internal] [puppetlabs.trapperkeeper.services :refer [service service-id]] [puppetlabs.trapperkeeper.testutils.bootstrap :as testutils] [puppetlabs.trapperkeeper.testutils.logging :as logging] [schema.test :as schema-test])) (use-fixtures :once with-no-jvm-shutdown-hooks schema-test/validate-schemas) (defprotocol ShutdownTestService) (defprotocol ShutdownTestServiceWithFn (test-fn [this])) (deftest shutdown-test (testing "service with shutdown hook gets called during shutdown" (let [shutdown-called? (atom false) test-service (service [] (stop [this context] (reset! shutdown-called? true) context)) app (testutils/bootstrap-services-with-empty-config [test-service])] (is (false? @shutdown-called?)) (internal/shutdown! (tk-app/app-context app)) (is (true? @shutdown-called?))))) (deftest shutdown-dep-order-test (testing "services are shut down in dependency order" (let [order (atom []) service1 (service ShutdownTestService [] (stop [this context] (swap! order conj 1) context)) service2 (service [[:ShutdownTestService]] (stop [this context] (swap! order conj 2) context)) app (testutils/bootstrap-services-with-empty-config [service1 service2])] (is (empty? @order)) (internal/shutdown! (tk-app/app-context app)) (is (= @order [2 1]))))) (deftest shutdown-continue-after-ex-test (testing "services continue to shut down when one throws an exception" (let [shutdown-called? (atom false) test-service (service [] (stop [this context] (reset! shutdown-called? true) context)) broken-service (service [] (stop [this context] (throw (RuntimeException. "dangit")))) app (testutils/bootstrap-services-with-empty-config [test-service broken-service])] (is (false? @shutdown-called?)) (logging/with-test-logging (let [errors (internal/shutdown! (tk-app/app-context app))] (is (= '("dangit") (map #(.getMessage ^Throwable %) errors)))) (is (logged? #"Encountered error during shutdown sequence" :error))) (is (true? @shutdown-called?))))) (deftest shutdown-run-app-test (testing "`tk/run-app` runs the framework (blocking until shutdown signal received), and `request-shutdown` shuts down services" (let [shutdown-called? (atom false) test-service (service [] (stop [this context] (reset! shutdown-called? true) context)) app (testutils/bootstrap-services-with-empty-config [test-service]) shutdown-svc (tk-app/get-service app :ShutdownService)] (is (false? @shutdown-called?)) (internal/request-shutdown shutdown-svc) (tk/run-app app) (is (true? @shutdown-called?))))) (deftest shutdown-on-error-custom-fn-error-test (testing (str "`shutdown-on-error` in custom function causes services to be " "shut down and the error is rethrown from main") (let [shutdown-called? (atom false) test-service (service ShutdownTestServiceWithFn [[:ShutdownService shutdown-on-error]] (stop [this context] (reset! shutdown-called? true) context) (test-fn [this] (future (shutdown-on-error (service-id this) #(throw (Throwable. "oops")))))) app (tk/boot-services-with-config [test-service] {}) test-svc (tk-app/get-service app :ShutdownTestServiceWithFn)] (is (false? @shutdown-called?)) (test-fn test-svc) (is (thrown-with-msg? Throwable #"oops" (tk/run-app app))) (is (true? @shutdown-called?))))) (defn bootstrap-and-validate-shutdown [services shutdown-called? expected-exception-message] (let [app (tk/boot-services-with-config services {})] (is (thrown-with-msg? Throwable expected-exception-message (tk/run-app app)) "tk run-app did not die with expected exception.") (is (true? @shutdown-called?) "Service shutdown was not called."))) (deftest shutdown-ex-during-init-test (testing (str "shutdown will be called if a service throws an exception " "during init on main thread, with appropriate errors logged") (logging/with-test-logging (let [shutdown-called? (atom false) test-service (service ShutdownTestService [] (init [this context] (throw (Throwable. "oops")) context) (stop [this context] (reset! shutdown-called? true) context))] (bootstrap-and-validate-shutdown [test-service] shutdown-called? #"oops") (is (logged? #"Error during service init!!!" :error) "Error message for service init not logged."))))) (deftest shutdown-ex-during-start-test (testing (str "shutdown will be called if a service throws an exception " "during start on main thread, with appropriate errors logged") (logging/with-test-logging (let [shutdown-called? (atom false) test-service (service ShutdownTestService [] (start [this context] (throw (Throwable. "oops")) context) (stop [this context] (reset! shutdown-called? true) context))] (bootstrap-and-validate-shutdown [test-service] shutdown-called? #"oops") (is (logged? #"Error during service start!!!" :error) "Error message for service start not logged."))))) (deftest shutdown-log-errors-during-init-test (testing (str "`shutdown-on-error` will catch and log errors raised during " "init on main thread") (logging/with-test-logging (let [shutdown-called? (atom false) test-service (service ShutdownTestService [[:ShutdownService shutdown-on-error]] (init [this context] (shutdown-on-error :ShutdownTestService #(throw (Throwable. "oops"))) context) (stop [this context] (reset! shutdown-called? true) context))] (bootstrap-and-validate-shutdown [test-service] shutdown-called? #"oops") (is (logged? #"shutdown-on-error triggered because of exception!" :error) "Error message for shutdown-on-error not logged."))))) (deftest shutdown-log-errors-during-init-future-test (testing (str "`shutdown-on-error` will catch and log errors raised during " "init on future") (logging/with-test-logging (let [shutdown-called? (atom false) test-service (service ShutdownTestService [[:ShutdownService shutdown-on-error]] (init [this context] @(future (shutdown-on-error :ShutdownTestService #(throw (Throwable. "oops")))) context) (stop [this context] (reset! shutdown-called? true) context))] (bootstrap-and-validate-shutdown [test-service] shutdown-called? #"oops") (is (logged? #"shutdown-on-error triggered because of exception!" :error) "Error message for shutdown-on-error not logged."))))) (deftest shutdown-log-errors-during-start-test (testing (str "`shutdown-on-error` will catch and log errors raised during" "start on main thread") (logging/with-test-logging (let [shutdown-called? (atom false) test-service (service ShutdownTestService [[:ShutdownService shutdown-on-error]] (start [this context] (shutdown-on-error :ShutdownTestService #(throw (Throwable. "oops"))) context) (stop [this context] (reset! shutdown-called? true) context))] (bootstrap-and-validate-shutdown [test-service] shutdown-called? #"oops") (is (logged? #"shutdown-on-error triggered because of exception!" :error) "Error message for shutdown-on-error not logged."))))) (deftest shutdown-log-errors-during-start-future-test (testing (str "`shutdown-on-error` will catch and log errors raised during " "start on future") (logging/with-test-logging (let [shutdown-called? (atom false) test-service (service ShutdownTestService [[:ShutdownService shutdown-on-error]] (start [this context] @(future (shutdown-on-error :ShutdownTestService #(throw (Throwable. "oops")))) context) (stop [this context] (reset! shutdown-called? true) context))] (bootstrap-and-validate-shutdown [test-service] shutdown-called? #"oops") (is (logged? #"shutdown-on-error triggered because of exception!" :error) "Error message for shutdown-on-error not logged."))))) (deftest shutdown-on-error-callback-test (testing "`shutdown-on-error` takes an optional function that is called on error" (let [shutdown-called? (atom false) on-error-fn-called? (atom false) broken-service (service ShutdownTestServiceWithFn [[:ShutdownService shutdown-on-error]] (stop [this context] (reset! shutdown-called? true) context) (test-fn [this] (shutdown-on-error (service-id this) #(throw (RuntimeException. "uh oh")) (fn [_ctxt] (reset! on-error-fn-called? true))))) app (testutils/bootstrap-services-with-empty-config [broken-service]) test-svc (tk-app/get-service app :ShutdownTestServiceWithFn)] (is (false? @shutdown-called?)) (is (false? @on-error-fn-called?)) (test-fn test-svc) (is (thrown-with-msg? RuntimeException #"uh oh" (tk/run-app app))) (is (true? @shutdown-called?)) (is (true? @on-error-fn-called?))))) (deftest shutdown-on-error-callback-error-test (testing "errors thrown by the `shutdown-on-error` optional on-error function are caught and logged" (let [broken-service (service ShutdownTestServiceWithFn [[:ShutdownService shutdown-on-error]] (test-fn [this] (shutdown-on-error (service-id this) #(throw (Throwable. "foo")) (fn [_ctxt] (throw (Throwable. "busted on-error function")))))) app (testutils/bootstrap-services-with-empty-config [broken-service]) test-svc (tk-app/get-service app :ShutdownTestServiceWithFn)] (logging/with-test-logging (test-fn test-svc) (is (thrown-with-msg? Throwable #"foo" (tk/run-app app))) (is (logged? #"Error occurred during shutdown" :error)))))) (deftest shutdown-on-error-error-handling (testing "Shutdown-on-error should never throw an exception." (testing "providing `nil` for all arguments" (let [test-service (tk/service [[:ShutdownService shutdown-on-error]] (init [this context] (shutdown-on-error nil nil nil) context))] (is (not (nil? (tk/boot-services-with-config [test-service] {})))))) (testing "passing `nil` instead of a function" (let [test-service (tk/service [[:ShutdownService shutdown-on-error]] (init [this context] (shutdown-on-error context nil) context))] (is (not (nil? (tk/boot-services-with-config [test-service] {})))))))) (deftest app-check-for-errors!-tests (testing "check-for-errors! throws exception for shutdown-on-error in init" (let [test-service (service ShutdownTestService [[:ShutdownService shutdown-on-error]] (init [this context] (shutdown-on-error :ShutdownTestService #(throw (Throwable. "oops"))) context)) app (tk/boot-services-with-config [test-service] {})] (is (thrown-with-msg? Throwable #"oops" (tk-app/check-for-errors! app)) "Expected error not thrown for check-for-errors!"))) (testing "check-for-errors! throws exception for error in init" (let [test-service (service ShutdownTestService [] (init [this context] (throw (Throwable. "oops")) context)) app (tk/boot-services-with-config [test-service] {})] (is (thrown-with-msg? Throwable #"oops" (tk-app/check-for-errors! app)) "Expected error not thrown for check-for-errors!"))) (testing "check-for-errors! throws exception for shutdown-on-error in start" (let [test-service (service ShutdownTestService [[:ShutdownService shutdown-on-error]] (start [this context] (shutdown-on-error :ShutdownTestService #(throw (Throwable. "oops"))) context)) app (tk/boot-services-with-config [test-service] {})] (is (thrown-with-msg? Throwable #"oops" (tk-app/check-for-errors! app)) "Expected error not thrown for check-for-errors!"))) (testing "check-for-errors! throws exception for error in start" (let [test-service (service ShutdownTestService [] (start [this context] (throw (Throwable. "oops")) context)) app (tk/boot-services-with-config [test-service] {})] (is (thrown-with-msg? Throwable #"oops" (tk-app/check-for-errors! app)) "Expected error not thrown for check-for-errors!"))) (testing "check-for-errors! returns app when no shutdown-on-error occurs" (let [test-service (service ShutdownTestService [] (init [this context] context)) app (tk/boot-services-with-config [test-service] {})] (is (identical? app (tk-app/check-for-errors! app)) "app not returned for check-for-errors!")))) (deftest shutdown-during-restart-test (testing "shutdown can't begin while restart or other lifecycle functions are in progress" (let [first-stop-begun? (promise) stop-should-proceed? (promise) lifecycle-events (atom []) svc (tk/service [[:ShutdownService request-shutdown]] (init [this context] (swap! lifecycle-events conj :init) context) (start [this context] (swap! lifecycle-events conj :start) context) (stop [this context] (swap! lifecycle-events conj :stop) ;; request shutdown, which will trigger the shutdown logic ;; on the main thread. (request-shutdown) (deliver first-stop-begun? true) @stop-should-proceed? context)) app (testutils/bootstrap-services-with-config [svc] {}) ;; this ensures that the 'main' shutdown logic will be runnable, ;; and gives us a way to observe when it has completed. app-main-thread (future (tk/run-app app)) shutdown-service (tk-app/get-service app :ShutdownService)] ;; no shutdown requested yet (is (nil? (internal/get-shutdown-reason shutdown-service))) ;; now we trigger a restart, which will call 'stop' for the first time, ;; which will request a shutdown but will block on the stop-should-proceed ;; promise (internal/restart-tk-apps [app]) ;; wait until we know that the shutdown has been requested @first-stop-begun? ;; we want to give the main thread a little time in case it is going to ;; do anything nefarious (as it would have before we consolidated the ;; shutdown logic into the core.async worker loop) (Thread/sleep 100) ;; at this point, the app's shutdown-reason-promise should be set (is (not (nil? (internal/get-shutdown-reason shutdown-service)))) ;; validate that the first three events are as expected (we're still blocked ;; in the first 'stop'). (is (= [:init :start :stop] @lifecycle-events)) ;; main thread should still be blocked (is (not (realized? app-main-thread))) ;; unblock the first 'stop' (deliver stop-should-proceed? true) ;; now wait for us to get all the way through the main thread @app-main-thread ;; validate that the restart completed before the shutdown called 'stop' ;; for a second time. (let [expected-lifecycle-events [:init :start :stop :init :start :stop]] (while (< (count @lifecycle-events) (count expected-lifecycle-events)) (Thread/yield)) (is (= expected-lifecycle-events @lifecycle-events)))))) (deftest shutdown-prioritized-test (testing "shutdown requests are prioritized over other pending lifecycle actions" (let [first-stop-begun? (promise) stop-should-proceed? (promise) lifecycle-events (atom []) svc (tk/service [] (init [this context] (swap! lifecycle-events conj :init) context) (start [this context] (swap! lifecycle-events conj :start) context) (stop [this context] (swap! lifecycle-events conj :stop) (deliver first-stop-begun? true) @stop-should-proceed? context)) app (testutils/bootstrap-services-with-config [svc] {}) ;; this ensures that the 'main' shutdown logic will be runnable, ;; and gives us a way to observe when it has completed. app-main-thread (future (tk/run-app app)) shutdown-service (tk-app/get-service app :ShutdownService)] ;; no shutdown requested yet (is (nil? (internal/get-shutdown-reason shutdown-service))) ;; now we trigger a restart, which will call 'stop' for the first time, ;; which will block on the stop-should-proceed promise (internal/restart-tk-apps [app]) ;; wait until we know that the stop has begun @first-stop-begun? ;; request a few more restarts, which should be queued up (internal/restart-tk-apps [app]) (internal/restart-tk-apps [app]) ;; request a shutdown, which should supercede any of the queued restarts (internal/request-shutdown shutdown-service) ;; at this point, the app's shutdown-reason-promise should be set (is (not (nil? (internal/get-shutdown-reason shutdown-service)))) ;; validate that the first three events are as expected (we're still blocked ;; in the first 'stop'). (is (= [:init :start :stop] @lifecycle-events)) ;; main thread should still be blocked (is (not (realized? app-main-thread))) ;; unblock the first 'stop' (deliver stop-should-proceed? true) ;; now wait for us to get all the way through the main thread @app-main-thread ;; validate that the first restart completed, and then we went directly to ;; the shutdown without processing the queued restarts. (let [expected-lifecycle-events [:init :start :stop :init :start :stop]] (is (= expected-lifecycle-events @lifecycle-events)))))) (deftest shutdown-called-twice-test (testing "app shuts down cleanly if shutdown is called twice" (let [first-stop-begun? (promise) stop-should-proceed? (promise) lifecycle-events (atom []) svc (tk/service [] (init [this context] (swap! lifecycle-events conj :init) context) (start [this context] (swap! lifecycle-events conj :start) context) (stop [this context] (swap! lifecycle-events conj :stop) (deliver first-stop-begun? true) @stop-should-proceed? context)) app (testutils/bootstrap-services-with-config [svc] {}) ;; request a stop on two separate threads. do these on futures ;; so that we can observe their progress. stop1-thread (future (tk-app/stop app)) stop2-thread (future (tk-app/stop app))] ;; wait until we know that the stop has begun @first-stop-begun? ;; validate that the first three events are as expected (we're still blocked ;; in the first 'stop'). (is (= [:init :start :stop] @lifecycle-events)) ;; stop threads should both be blocked (is (not (realized? stop1-thread))) (is (not (realized? stop2-thread))) ;; unblock the first 'stop' (deliver stop-should-proceed? true) ;; now wait for both stop threads to complete @stop1-thread @stop2-thread ;; validate that the first stop completed, and that the second was a no-op (let [expected-lifecycle-events [:init :start :stop]] (is (= expected-lifecycle-events @lifecycle-events)))))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/signal_handling_test.clj000066400000000000000000000021741463756611100317650ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.signal-handling-test (:require [puppetlabs.trapperkeeper.core :as core])) (defn- start-test [context get-in-config] (let [continue? (atom true) thread (future (try ;; future just discards top-level exceptions (while @continue? (let [target (get-in-config [:signal-test-target])] (assert target) (Thread/sleep 200) (spit target "exciting"))) (catch Throwable ex (prn ex) (throw ex))))] (assoc context :finish-signal-test (fn exit-signal-test [] (reset! continue? false) @thread)))) (defn- stop-test [{:keys [finish-signal-test] :as context}] (finish-signal-test) context) (defprotocol SignalHandlingTestService) (core/defservice signal-handling-test-service SignalHandlingTestService [[:ConfigService get-in-config]] (init [this context] context) (start [this context] (start-test context get-in-config)) (stop [this context] (stop-test context))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/testutils/000077500000000000000000000000001463756611100271475ustar00rootroot00000000000000puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/testutils/bootstrap.clj000066400000000000000000000053161463756611100316630ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.testutils.bootstrap (:require [me.raynes.fs :as fs] [puppetlabs.trapperkeeper.core :as tk] [puppetlabs.trapperkeeper.app :as tk-app] [puppetlabs.kitchensink.testutils :as ks-testutils] [puppetlabs.trapperkeeper.bootstrap :as bootstrap] [puppetlabs.trapperkeeper.config :as config] [puppetlabs.trapperkeeper.internal :as internal])) (def empty-config "./target/empty.ini") (fs/touch empty-config) (defn bootstrap-services-with-config [services config] (internal/throw-app-error-if-exists! (tk/boot-services-with-config services config))) (defmacro with-app-with-config [app services config & body] `(ks-testutils/with-no-jvm-shutdown-hooks (let [~app (bootstrap-services-with-config ~services ~config)] (try ~@body (finally (tk-app/stop ~app true)))))) (defn bootstrap-services-with-cli-data [services cli-data] (internal/throw-app-error-if-exists! (tk/boot-services-with-config-fn services #(config/parse-config-data cli-data)))) (defmacro with-app-with-cli-data [app services cli-data & body] `(ks-testutils/with-no-jvm-shutdown-hooks (let [~app (bootstrap-services-with-cli-data ~services ~cli-data)] (try ~@body (finally (tk-app/stop ~app true)))))) (defn bootstrap-services-with-cli-args [services cli-args] (bootstrap-services-with-cli-data services (internal/parse-cli-args! cli-args))) (defmacro with-app-with-cli-args [app services cli-args & body] `(ks-testutils/with-no-jvm-shutdown-hooks (let [~app (bootstrap-services-with-cli-args ~services ~cli-args)] (try ~@body (finally (tk-app/stop ~app true)))))) (defn bootstrap-services-with-empty-config [services] (bootstrap-services-with-cli-data services {:config empty-config})) (defmacro with-app-with-empty-config [app services & body] `(ks-testutils/with-no-jvm-shutdown-hooks (let [~app (bootstrap-services-with-empty-config ~services)] (try ~@body (finally (tk-app/stop ~app true)))))) (defn bootstrap-with-empty-config ([] (bootstrap-with-empty-config [])) ([other-args] (-> other-args (conj "--config" empty-config) (internal/parse-cli-args!) (tk/boot-with-cli-data) (internal/throw-app-error-if-exists!)))) (defn parse-and-bootstrap ([bootstrap-config] (parse-and-bootstrap bootstrap-config {:config empty-config})) ([bootstrap-config cli-data] (-> bootstrap-config (bootstrap/parse-bootstrap-config!) (bootstrap-services-with-cli-data cli-data)))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/testutils/logging.clj000066400000000000000000000523111463756611100312710ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.testutils.logging (:require [clojure.set :as set] [clojure.test] [clojure.tools.logging.impl :as impl] [me.raynes.fs :as fs] [puppetlabs.kitchensink.core :as kitchensink] [puppetlabs.trapperkeeper.logging :as pl-log :refer [root-logger-name]] [schema.core :as s]) (:import (ch.qos.logback.classic Level) (ch.qos.logback.classic.encoder PatternLayoutEncoder) (ch.qos.logback.core Appender FileAppender) (ch.qos.logback.core AppenderBase) (ch.qos.logback.core.spi LifeCycle) (java.util.regex Pattern) (org.slf4j LoggerFactory))) ;; Note that the logging configuration is a global resource, so ;; simultaneous calls to reset-logging, configure-logging!, etc. may ;; interfere with many of these calls. (def ^:private keyword-levels {:trace Level/TRACE :debug Level/DEBUG :info Level/INFO :warn Level/WARN :error Level/ERROR}) (def ^:private level-keywords (set/map-invert keyword-levels)) (def ^:private levels (set (keys keyword-levels))) (defn event->map "Returns {:logger name :level lvl :exception throwable :message msg} for the given event. Note that this does not convert any nil messages to \"\"." [event] {:logger (.getLoggerName event) :level (level-keywords (.getLevel event)) :message (.getFormattedMessage event) :exception (.getThrowableProxy event)}) ;; Perhaps make call-with-started and with-started public in ;; kitchensink or some other namespace? (defn- call-with-started [objs f] (if-not (seq objs) (f) (let [[obj & remainder] objs] (try (call-with-started remainder f) (finally (.stop obj)))))) (defmacro ^:private with-started "Ensures that if a given name's init form executes without throwing an exception, (.stop name) will be called before returning from with-started. It is the responsibility of the init form to make sure the object has been started. This macro behaves like with-open, but with respect to .stop instead of .close." [bindings & body] (let [names (take-nth 2 bindings) initializers (take-nth 2 (rest bindings))] `(let [started-objs# [~@initializers]] (#'puppetlabs.trapperkeeper.testutils.logging/call-with-started started-objs# (fn [] (apply (fn [~@names] ~@body) started-objs#)))))) (defn- find-logger [id] (LoggerFactory/getLogger (if (class? id) id (str id)))) (defn call-with-log-level "Sets the (logback) log level for the logger specified by logger-id during the evaluation of f. If logger-id is not a class, it will be converted via str, and the level must be a clojure.tools.logging key, i.e. :info, :error, etc." [logger-id level f] ;; Assumes use of logback (i.e. logger supports Levels). (let [logger (find-logger logger-id) original-level (.getLevel logger)] (try (.setLevel logger (level keyword-levels)) (f) (finally (.setLevel logger original-level))))) (defmacro with-log-level "Sets the (logback) log level for the logger specified by logger-id during the evaluation of body. If logger-id is not a class, it will be converted via str, and the level must be a clojure.tools.logging key, i.e. :info, :error, etc." [logger-id level & body] `(call-with-log-level ~logger-id ~level (fn [] ~@body))) (defn- log-event-listener "Returns a log Appender that will call (listen event) for each log event." [listen] ;; No clue yet if we're supposed to start with a default name. (let [name (atom (str "tk-log-listener-" (kitchensink/uuid))) started? (atom false)] (reify Appender (doAppend [_this event] (when @started? (listen event))) (getName [_this] @name) (setName [_this x] (reset! name x)) LifeCycle (start [_this] (reset! started? true)) (stop [_this] (reset! started? false)) (isStarted [_this] @started?)))) (defn call-with-additional-log-appenders "Adds the specified appenders to the logger specified by logger-id, calls f, and then removes them. If logger-id is not a class, it will be converted via str." [logger-id appenders f] (let [logger (find-logger logger-id)] (try (doseq [appender appenders] (.addAppender logger appender)) (f) (finally (doseq [appender appenders] (.detachAppender logger appender)))))) (defmacro with-additional-log-appenders "Adds the specified appenders to the logger specified by logger-id, evaluates body, and then removes them. If logger-id is not a class, it will be converted via str." [logger-id appenders & body] `(call-with-additional-log-appenders ~logger-id ~appenders (fn [] ~@body))) (defn call-with-log-appenders "Replaces the appenders of the logger specified by logger-id with the specified appenders, calls f, and then restores the original appenders. If logger-id is not a class, it will be converted via str." [logger-id appenders f] (let [logger (find-logger logger-id) original-appenders (iterator-seq (.iteratorForAppenders logger))] (try (doseq [appender original-appenders] (.detachAppender logger appender)) (call-with-additional-log-appenders logger-id appenders f) (finally (doseq [appender original-appenders] (.addAppender logger appender)))))) (defmacro with-log-appenders "Replaces the appenders of the logger specified by logger-id with the specified appenders, evaluates body, and then restores the original appenders. If logger-id is not a class, it will be converted via str." [logger-id appenders & body] `(call-with-log-appenders ~logger-id ~appenders (fn [] ~@body))) (defn call-with-additional-log-event-listeners "For each listen in listens, calls (listen event) for each logger-id event produced during the evaluation of f. If logger-id is not a class, it will be converted via str." [logger-id listens f] (letfn [(set-up [listens listeners] (if-not (seq listens) (call-with-additional-log-appenders logger-id listeners f) (let [[listen & remainder] listens] (with-started [listener (doto (log-event-listener listen) .start)] (set-up remainder (conj listeners listener))))))] (set-up listens []))) (defmacro with-additional-log-event-listeners "For each listen in listens, calls (listen event) for each logger-id event produced during the evaluation of body. If logger-id is not a class, it will be converted via str." [logger-id listens & body] `(call-with-additional-log-event-listeners ~logger-id ~listens (fn [] ~@body))) (defn call-with-log-event-listeners "For each listen in listens, calls (listen event) for each logger-id event produced during the evaluation of f, after removing any existing log appenders. If logger-id is not a class, it will be converted via str." [logger-id listens f] (letfn [(set-up [listens listeners] (if-not (seq listens) (call-with-log-appenders logger-id listeners f) (let [[listen & remainder] listens] (with-started [listener (doto (log-event-listener listen) .start)] (set-up remainder (conj listeners listener))))))] (set-up listens []))) (defmacro with-log-event-listeners "For each listen in listens, calls (listen event) for each logger-id event produced during the evaluation of body, after removing any existing log appenders. If logger-id is not a class, it will be converted via str." [logger-id listens & body] `(call-with-log-event-listeners ~logger-id ~listens (fn [] ~@body))) (defmacro with-additional-logging-to-atom "Conjoins all logger-id events produced during the evaluation of the body onto the collection in the destination atom. If logger-id is not a class, it will be converted via str." [logger-id destination & body] `(with-additional-log-event-listeners ~logger-id [(fn [event#] (swap! ~destination conj event#))] ~@body)) (defmacro with-logging-to-atom "Conjoins all logger-id events produced during the evaluation of the body onto the collection in the destination atom, after removing any existing log appenders. If logger-id is not a class, it will be converted via str. For simple situations, with-logged-event-maps may be more convenient." [logger-id destination & body] `(with-log-event-listeners ~logger-id [(fn [event#] (swap! ~destination conj event#))] ~@body)) (defmacro with-logger-event-maps "After removing any existing log appenders, binds event-maps to an atom containing a collection, and then appends a map to that collection for each event logged to logger-id during the evaluation of the body. See event->map for the map structure. If logger-id is not a class, it will be converted via str." [logger-id event-maps & body] `(let [dest# (atom []) ~event-maps dest#] (with-log-event-listeners ~logger-id [(fn [event#] (swap! dest# conj (event->map event#)))] ~@body))) (defmacro with-logged-event-maps "After removing any existing log appenders, binds event-maps to an atom containing a collection, and then appends a map to that collection for each event logged to root-logger-name during the evaluation of the body. See event->map for the map structure." [event-maps & body] `(with-logger-event-maps root-logger-name ~event-maps ~@body)) (defn- suppressing-file-appender [log-path] (let [pattern "%-4relative [%thread] %-5level %logger{35} - %msg%n" context (LoggerFactory/getILoggerFactory)] (doto (FileAppender.) (.setFile log-path) (.setAppend true) (.setEncoder (doto (PatternLayoutEncoder.) (.setPattern pattern) (.setContext context) (.start))) (.setContext context) (.start)))) (defn call-with-log-suppressed-unless-notable [pred f] (let [problem (atom false) log-path (kitchensink/absolute-path (fs/temp-file "tk-suppressed" ".log"))] (try (with-started [appender (suppressing-file-appender log-path) detector (doto (log-event-listener (fn [event] (when (pred event) (reset! problem true)))) .start)] (with-log-appenders root-logger-name [appender detector] (f))) (finally (if @problem (binding [*out* *err*] (print (slurp log-path)) (println "From error log:" log-path)) (fs/delete log-path)))))) (defmacro with-log-suppressed-unless-notable "Executes the body with all logging suppressed, and passes every log event to pred. Dumps the full log to *err* along with its path if, and only if, any invocation of pred returns a true value, . Assumes that the logging level is already set as desired. This may not work correctly if the system logback config is altered during the execution of the body." [pred & body] `(call-with-log-suppressed-unless-notable ~pred (fn [] ~@body))) (def ^{:doc "An atom containing a sequence of all of the log event maps recorded during an evaluation of with-test-logging." :dynamic true :private true} *test-log-events* nil) (defmacro with-test-logging "Creates an environment for the use of the logged? test method." [& body] `(let [destination# (atom [])] (binding [*test-log-events* destination#] (with-redefs [pl-log/configure-logger! (fn [& _#])] (with-log-level root-logger-name :trace (with-logging-to-atom root-logger-name destination# ~@body)))))) (defmacro with-test-logging-debug "Creates an environment for the use of the logged? test method, and arranges for every event map logged within that environment to be printed to *err*." [& body] `(let [destination# (atom [])] (binding [*test-log-events* destination#] (with-log-level root-logger-name :trace (with-log-event-listeners root-logger-name [(fn [event#] (binding [*out* *err*] (println "** Log entry:" (pr-str (event->map event#)))) (swap! destination# conj event#))] ~@body))))) (s/defn ^{:always-validate true} logged? ([msg-or-pred] (logged? msg-or-pred nil nil)) ([msg-or-pred maybe-level] (logged? msg-or-pred maybe-level nil)) ([msg-or-pred :- (s/conditional ifn? (s/pred ifn?) string? s/Str :else Pattern) maybe-level :- (s/maybe (s/pred #(levels %))) disable-single-line-match-restriction :- (s/maybe s/Bool)] (let [match? (cond (ifn? msg-or-pred) msg-or-pred (string? msg-or-pred) #(= msg-or-pred (:message %)) :else #(re-find msg-or-pred (:message %))) one-element-if-specified? (fn [items] (if (seq items) (if (or disable-single-line-match-restriction (empty? (rest items))) true (do (println "\n`logged?` warning: multiple log line matches found, but this arity expects only one match, returning false. Found matches: \n" (pr-str (map :message items)) "\n") false)) false)) correct-level? #(or (nil? maybe-level) (= maybe-level (:level %)))] (->> (map event->map @*test-log-events*) (filter correct-level?) (filter match?) (one-element-if-specified?))))) (defmethod clojure.test/assert-expr 'logged? [is-msg form] ;"Asserts that exactly one event in *test-log-events* has a message ;that matches msg-or-pred. The match is performed via = if ;msg-or-pred is a string, via re-find if msg-or-pred is a pattern, or ;via (msg-or-pred event-map) if msg-or-pred is a function. If level ;is specified, the message's keyword level (:info, :error, etc.) must ;also match. For example: ; (with-test-logging (log/info \"123\") (is (logged? #\"2\")))." (assert (#{2 3 4} (count form))) (let [[_ msg-or-pred level disable-single-line-restriction] form] `(let [events# @@#'puppetlabs.trapperkeeper.testutils.logging/*test-log-events*] (if-not (logged? ~msg-or-pred ~level ~disable-single-line-restriction) (clojure.test/do-report {:type :fail :message ~is-msg :expected '~form :actual (list '~'logged events#)}) (clojure.test/do-report {:type :pass :message ~is-msg :expected '~form :actual (list '~'logged events#)}))))) (defn reset-logging-config-after-test "Fixture that will reset the logging configuration after each test. Useful for tests that manipulate the logging configuration, in order to ensure that they don't affect test logging for subsequent tests." [f] (f) (pl-log/reset-logging)) ;;; Deprecated API (clojure.tools.logging specific, etc.) (def ^{:doc "A dynamic var that is bound to an atom containing all of the log entries that have occurred during a test, when using `with-test-logging`." :dynamic true :deprecated "1.1.2"} *test-logs* nil) (def ^{:deprecated "1.1.2"} legal-levels #{nil :trace :debug :info :warn :error :fatal}) (defn- ^{:deprecated "1.1.2"} log-entry->map [log-entry] {:namespace (get log-entry 0) :level (get log-entry 1) :exception (get log-entry 2) :message (get log-entry 3)}) (defn ^{:deprecated "1.1.2"} logs-matching "Given a regular expression pattern, a sequence of log messages (in the format used by `clojure.tools.logging`, and (optionally) a log level (as a keyword that corresponds to those in `clojure.tools.logging`) return only the logs whose message matches the specified regular expression pattern that were logged at the given level (or at any level if not specified). (Intended to be used alongside `with-log-output` for tests that are validating log output.) The result is a sequence of maps, each of which contains the following keys: `:namespace`, `:level`, `:exception`, and `:message`." ([pattern logs] (logs-matching pattern logs nil)) ([pattern logs level] {:pre [(instance? java.util.regex.Pattern pattern) (coll? logs) #_:clj-kondo/ignore (contains? legal-levels level)]} ;; the logs are formatted as sequences, where the keyword at index 1 ;; contains the level and the string at index 3 contains the actual ;; log message. (let [matches-level? (fn [log-entry] (or (nil? level) (= level (get log-entry 1)))) matches-msg? (fn [log-entry] (re-find pattern (get log-entry 3))) matches (filter #(and (matches-level? %) (matches-msg? %)) logs)] #_:clj-kondo/ignore (map log-entry->map matches)))) (defn ^{:deprecated "1.1.2"} log-to-console "Utility function called by atom-logger and atom-appender to log entries to the console when running in debug mode." [entry] (println "** Log entry:" entry)) (defn ^{:deprecated "1.1.2"} atom-logger "Returns a logger factory that returns loggers that conjoin each log event onto the collection in the destination atom, and that also invoke (log-to-console event) if debug? is true. This will only capture events logged by clojure.tools.logging, and the events will be vectors like this [namespace level exception message]. Prefer with-logging-to-atom." ([destination] (atom-logger destination false)) ([destination debug?] (reify impl/LoggerFactory (name [_] "test factory") (get-logger [_ log-ns] (reify impl/Logger (enabled? [_ _level] true) (write! [_ lvl ex msg] (let [entry [(str log-ns) lvl ex msg]] #_:clj-kondo/ignore (when debug? (log-to-console entry)) (swap! destination conj entry) nil))))))) (defn ^{:deprecated "1.1.2"} atom-appender "Returns a log Appender that conjoins each log event to the collection in the destination atom, and that also invokes (log-to-console event) if debug? is true. Prefer with-logging-to-atom." ([destination] (atom-appender destination false)) ([destination debug?] (let [appender (proxy [AppenderBase] [] (append [logging-event] (let [throwable-info (.getThrowableInformation logging-event) ex (when throwable-info (.getThrowable throwable-info)) entry [(.getLoggerName logging-event) (.getLevel logging-event) ex (str (.getFormattedMessage logging-event))]] #_:clj-kondo/ignore (when debug? (log-to-console entry)) (swap! destination conj entry))) (close []))] (.setContext appender (pl-log/logging-context)) appender))) (defmacro ^{:deprecated "1.1.2"} with-log-output-atom "This is a utility macro, intended for use by other macros such as `with-test-logging`. Given an atom whose value is a sequence, sets up a temporary logger to capture all log output to the sequence, and evaluates `body` in this logging context. `log-output-atom` - Inside of `body`, this atom will be used to store the sequence of log messages that have been logged so far. You can access the individual log messages by dereferencing the atom. Prefer with-logging-to-atom." [log-output-atom options & body] `(let [root-logger# (pl-log/root-logger) orig-appenders# (vec (iterator-seq (.iteratorForAppenders root-logger#))) orig-started# (into {} (map #(vector % (.isStarted %)) orig-appenders#)) temp-appender# (atom-appender ~log-output-atom (~options :debug))] (.setName temp-appender# "testutils-temp-log-appender") (try (doseq [orig-appender# orig-appenders#] #_:clj-kondo/ignore (.stop orig-appender#)) (.addAppender root-logger# temp-appender#) (binding [clojure.tools.logging/*logger-factory* (atom-logger ~log-output-atom (~options :debug))] ~@body) (finally (.detachAppender root-logger# temp-appender#) (doseq [orig-appender# orig-appenders#] (if (orig-started# orig-appender#) (.start orig-appender#))))))) (defmacro ^{:deprecated "1.1.2"} with-log-output "Sets up a temporary logger to capture all log output to a sequence, and evaluates `body` in this logging context. `log-output-var` - Inside of `body`, the variable named `log-output-var` is a clojure atom containing the sequence of log messages that have been logged so far. You can access the individual log messages by dereferencing this variable (with either `deref` or `@`). Example: (with-log-output logs (log/info \"Hello There\") (is (= 1 (count (logs-matching #\"Hello There\" @logs)))))" [log-output-var & body] `(let [~log-output-var (atom [])] (with-log-output-atom ~log-output-var {:debug false} ~@body))) puppetlabs-trapperkeeper-d1f1135/test/puppetlabs/trapperkeeper/testutils/logging_test.clj000066400000000000000000000173011463756611100323300ustar00rootroot00000000000000(ns puppetlabs.trapperkeeper.testutils.logging-test (:require [clojure.test :refer :all] [clojure.tools.logging :as log] [puppetlabs.kitchensink.core :as kitchensink] [puppetlabs.trapperkeeper.logging :refer [reset-logging root-logger-name]] [puppetlabs.trapperkeeper.testutils.logging :as tgt :refer [event->map]]) (:import (org.slf4j LoggerFactory))) ;; Without this, "lein test NAMESPACE" and :only invocations may fail. (use-fixtures :once (fn [f] (reset-logging) (f))) (deftest with-log-level-and-logging-to-atom (let [expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" :level :info :message "wlta-test" :exception nil}] (let [log (atom [])] (tgt/with-log-level root-logger-name :error (tgt/with-logging-to-atom root-logger-name log (log/info "wlta-test")) (is (not-any? #(= expected %) (map event->map @log))))) (let [log (atom [])] (tgt/with-log-level root-logger-name :info (tgt/with-logging-to-atom root-logger-name log (log/info "wlta-test")) (is (some #(= expected %) (map event->map @log))))))) (def call-with-started #'puppetlabs.trapperkeeper.testutils.logging/call-with-started) (def find-logger #'puppetlabs.trapperkeeper.testutils.logging/find-logger) (def log-event-listener #'puppetlabs.trapperkeeper.testutils.logging/log-event-listener) (defn get-appenders [logger] (iterator-seq (.iteratorForAppenders logger))) (deftest with-additional-log-appenders (let [log (atom []) logger (find-logger root-logger-name) uuid (kitchensink/uuid) original-appenders (get-appenders logger) new-appender (doto (log-event-listener (fn [event] (swap! log conj event))) .start) expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" :level :error :message uuid :exception nil}] (call-with-started [new-appender] #(tgt/with-additional-log-appenders root-logger-name [new-appender] (is (= (set (cons new-appender original-appenders)) (set (get-appenders logger)))) (log/error uuid))) (is (= (set original-appenders) (set (get-appenders logger)))) (is (some #(= expected %) (map event->map @log))))) (deftest with-log-appenders (let [log (atom []) logger (find-logger root-logger-name) uuid (kitchensink/uuid) original-appenders (get-appenders logger) new-appender (doto (log-event-listener (fn [event] (swap! log conj event))) .start) expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" :level :error :message uuid :exception nil}] (call-with-started [new-appender] ;; ignore deprecation #_:clj-kondo/ignore #(tgt/with-log-appenders root-logger-name [new-appender] (is (= [new-appender] (get-appenders logger))) (log/error uuid))) (is (= (set original-appenders) (set (get-appenders logger)))) (is (some #(= expected %) (map event->map @log))))) (deftest with-log-event-listeners (let [log (atom []) uuid (kitchensink/uuid) expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" :level :info :message uuid :exception nil}] (tgt/with-log-level root-logger-name :info (tgt/with-log-event-listeners root-logger-name [(fn [event] (swap! log conj event))] (log/info uuid)) (is (some #(= expected %) (map event->map @log)))))) (deftest suppressing-log-unless-error (let [uuid (kitchensink/uuid) target (format "some random message %s" uuid)] (testing "log not dumped if uninteresting" (is (not (re-find (re-pattern target) (with-out-str (binding [*err* *out*] (tgt/with-log-suppressed-unless-notable (constantly false) (log/info target)))))))) (testing "log dumped if notable" (is (re-find (re-pattern target) (with-out-str (binding [*err* *out*] (tgt/with-log-suppressed-unless-notable #(= "lp0 on fire" (:message (event->map %))) (log/info target) (log/info "lp0 on fire"))))))))) (deftest with-test-logging (testing "basic matching" (doseq [[item test] [["foo" "foo" "barbar" #"rb" "baz" (fn [e] (and (= :trace (:level e)) (= "baz" (:message e))))]]] (tgt/with-test-logging (log/trace item) (is (logged? test))) (tgt/with-test-logging (log/trace "hapax legomenon") (is (not (tgt/logged? test)))))) (testing "level matches" (doseq [level @#'puppetlabs.trapperkeeper.testutils.logging/levels] (tgt/with-test-logging (log/log level "foo") (is (logged? "foo" level)))) ;; Does not match when logged above or below correct level (tgt/with-test-logging (log/debug "debug") (is (not (tgt/logged? #"debug" :warn)))) (tgt/with-test-logging (log/debug "debug") (is (not (tgt/logged? #"debug" :trace))))) (testing "captures parameterized slf4j messages" (tgt/with-test-logging (let [test-logger (LoggerFactory/getLogger "tk-test")] (.info test-logger "Log message: {}" "odelay") (is (tgt/logged? #"odelay")))))) (deftest with-test-logging-debug (testing "basic matching" (doseq [[item test] [["foo" "foo" "barbar" #"rb" "baz" (fn [e] (and (= :trace (:level e)) (= "baz" (:message e))))]]] (tgt/with-test-logging-debug (log/trace item) (is (logged? test))) (tgt/with-test-logging-debug (log/trace "hapax legomenon") (is (not (tgt/logged? test)))))) (testing "level matches" (doseq [level @#'puppetlabs.trapperkeeper.testutils.logging/levels] (tgt/with-test-logging-debug (log/log level "foo") (is (logged? "foo" level)))) (tgt/with-test-logging-debug (log/debug "debug") (is (not (tgt/logged? #"debug" :warn)))) (tgt/with-test-logging (log/debug "debug") (is (not (tgt/logged? #"debug" :trace))))) (testing "that events are logged to *err*" (tgt/with-test-logging-debug (let [err (with-out-str (binding [*err* *out*] (log/trace "foo")))] (is (re-matches #"\*\* Log entry: (.|\n)*" err)) (is (re-find #":logger " err)) (is (re-find #":level :trace" err)) (is (re-find #":exception nil" err)) (is (re-find #":message \"foo\"" err))) (is (logged? "foo"))))) (deftest with-logger-event-maps (let [expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" :level :error :message "wlgrem-test" :exception nil}] (tgt/with-logger-event-maps root-logger-name events (log/error "wlgrem-test") (is (some #(= expected %) @events))))) (deftest with-logged-event-maps (let [expected {:logger "puppetlabs.trapperkeeper.testutils.logging-test" :level :error :message "wlgdem-test" :exception nil}] (tgt/with-logged-event-maps events (log/error "wlgdem-test") (is (some #(= expected %) @events))))) puppetlabs-trapperkeeper-d1f1135/tk000077500000000000000000000030221463756611100174410ustar00rootroot00000000000000#!/usr/bin/env bash set -ueo pipefail usage() { echo "Usage: tk JVM_ARG ... -- TK_ARG ..."; } misuse() { usage 1>&2; exit 2; } jar_glob='trapperkeeper-*-SNAPSHOT-standalone.jar' # Believe last -cp wins for java, and here, any final -cp path will be # placed in front of the jar. cp='' jvm_args=() while test $# -gt 0; do case "$1" in -h|--help) usage exit 0 ;; -cp) shift test $# -gt 0 || misuse cp="$1" shift ;; --) shift break ;; *) shift jvm_args+=("$1") ;; esac done if test "${TRAPPERKEEPER_JAR:-}"; then jar="$TRAPPERKEEPER_JAR" else # Find the standalone jar and make sure there's only one. # FIXME: minor race here between find runs # Use a bash array expansion to count the files so we don't have # to worry about strange paths (though admittedly unlikely here). shopt -s nullglob jars=(target/$jar_glob) shopt -u nullglob if test "${#jars[@]}" -gt 1; then echo "error: found more than one SNAPSHOT jar:" 1>&2 find target -maxdepth 1 -name "$jar_glob" 1>&2 exit 2 fi jar="${jars[0]}" fi if ! test -e "$jar"; then printf 'Unable to find target/%s; have you run "lein uberjar"?\n' \ "$jar" 1>&2 exit 2 fi set -x if test "$cp"; then cp="$cp:$jar" else cp="$jar" fi exec java -cp "$cp" clojure.main -m puppetlabs.trapperkeeper.main "$@"